001/*
002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nicolas Chapurlat <nchapurlat@nuxeo.com>
018 */
019
020package org.nuxeo.ecm.core.io.registry.reflect;
021
022import java.lang.reflect.Constructor;
023import java.lang.reflect.Field;
024import java.lang.reflect.Modifier;
025import java.lang.reflect.Type;
026import java.lang.reflect.TypeVariable;
027import java.util.ArrayList;
028import java.util.List;
029import java.util.Map;
030
031import javax.inject.Inject;
032import javax.ws.rs.core.MediaType;
033
034import org.apache.commons.lang3.reflect.TypeUtils;
035import org.apache.commons.logging.Log;
036import org.apache.commons.logging.LogFactory;
037import org.nuxeo.ecm.core.api.DocumentModel;
038import org.nuxeo.ecm.core.api.NuxeoException;
039import org.nuxeo.ecm.core.api.NuxeoGroup;
040import org.nuxeo.ecm.core.api.NuxeoPrincipal;
041import org.nuxeo.ecm.core.api.model.Property;
042import org.nuxeo.ecm.core.io.registry.Marshaller;
043import org.nuxeo.ecm.core.io.registry.MarshallerRegistry;
044import org.nuxeo.ecm.core.io.registry.MarshallingException;
045import org.nuxeo.ecm.core.io.registry.Reader;
046import org.nuxeo.ecm.core.io.registry.Writer;
047import org.nuxeo.ecm.core.io.registry.context.RenderingContext;
048import org.nuxeo.ecm.core.io.registry.context.RenderingContextImpl;
049import org.nuxeo.ecm.core.io.registry.context.ThreadSafeRenderingContext;
050import org.nuxeo.runtime.api.Framework;
051
052/**
053 * Utility class used to instanciate marshallers. This class checks if a marshaller has annotation {@link Setup} and
054 * inspect every attributes having {@link Inject} annotation.
055 * <p>
056 * To get a valid marshaller instance :
057 * <ul>
058 * <li>Create an inspector for your marshaller class using constructor {@link #MarshallerInspector(Class)}. This will
059 * checks your marshaller has annotation {@link Setup} and inspect every attributes having {@link Inject}
060 * annotation.</li>
061 * <li>You can check it's a valid marshaller by calling @ #isValid()}</li>
062 * <li>You can check it's a {@link Writer} by calling {@link #isWriter()}</li>
063 * <li>You can check it's a {@link Reader} by calling {@link #isReader()}</li>
064 * <li>You can finally call {@link #getInstance(RenderingContext)} to get a valid marshaller instance with
065 * {@link RenderingContext} and required services injected.</li>
066 * </ul>
067 * <p>
068 * This class implements {@link Comparable} and then handle marshaller priorities rules: look at
069 * {@link MarshallerRegistry} javadoc to read the rules.
070 *
071 * @since 7.2
072 */
073public class MarshallerInspector implements Comparable<MarshallerInspector> {
074
075    private static final Log log = LogFactory.getLog(MarshallerInspector.class);
076
077    private Class<?> clazz;
078
079    private Integer priority;
080
081    private Instantiations instantiation;
082
083    private List<MediaType> supports = new ArrayList<>();
084
085    private Constructor<?> constructor;
086
087    private List<Field> serviceFields = new ArrayList<>();
088
089    private List<Field> contextFields = new ArrayList<>();
090
091    private Object singleton;
092
093    /**
094     * A boolean to save the service instrumentation state
095     */
096    private volatile boolean servicesInjected;
097
098    private ThreadLocal<Object> threadInstance;
099
100    private Class<?> marshalledType;
101
102    private Type genericType;
103
104    /**
105     * Create an inspector for the given class.
106     *
107     * @param clazz The class to analyse and instantiate.
108     */
109    public MarshallerInspector(Class<?> clazz) {
110        this.clazz = clazz;
111        load();
112    }
113
114    /**
115     * Introspect this marshaller: gets instantiation mode, supported mimetype, gets the managed class, generic type and
116     * load every needed injection to be ready to create an instance quickly.
117     *
118     * @since 7.2
119     */
120    private void load() {
121        // checks if there's a public constructor without parameters
122        for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
123            if (Modifier.isPublic(constructor.getModifiers()) && constructor.getParameterTypes().length == 0) {
124                this.constructor = constructor;
125                break;
126            }
127        }
128        if (constructor == null) {
129            throw new MarshallingException("No public constructor found for class " + clazz.getName()
130                    + ". Instanciation will not be possible.");
131        }
132        // load instantiation mode
133        Setup setup = loadSetup(clazz);
134        if (setup == null) {
135            throw new MarshallingException("No required @Setup annotation found for class " + clazz.getName()
136                    + ". Instanciation will not be possible.");
137        }
138        if (!isReader() && !isWriter()) {
139            throw new MarshallingException(
140                    "MarshallerInspector only supports Reader and Writer: you must implement one of this interface for this class: "
141                            + clazz.getName());
142        }
143        if (isReader() && isWriter()) {
144            throw new MarshallingException(
145                    "MarshallerInspector only supports either Reader or Writer: you must implement only one of this interface: "
146                            + clazz.getName());
147        }
148        instantiation = setup.mode();
149        priority = setup.priority();
150        // load supported mimetype
151        Supports supports = loadSupports(clazz);
152        if (supports != null) {
153            for (String mimetype : supports.value()) {
154                try {
155                    MediaType mediaType = MediaType.valueOf(mimetype);
156                    this.supports.add(mediaType);
157                } catch (IllegalArgumentException e) {
158                    log.warn("In marshaller class " + clazz.getName() + ", the declared mediatype " + mimetype
159                            + " cannot be parsed as a mimetype");
160                }
161            }
162        }
163        if (this.supports.isEmpty()) {
164            log.warn("The marshaller " + clazz.getName()
165                    + " does not support any mimetype. You can add some using annotation @Supports");
166        }
167        // loads the marshalled type and generic type
168        loadMarshalledType(clazz);
169        // load properties that require injection
170        loadInjections(clazz);
171        // warn if several context found
172        if (contextFields.size() > 1) {
173            log.warn("The marshaller " + clazz.getName()
174                    + " has more than one context injected property. You probably should use a context from a parent class.");
175        }
176        if (instantiation == Instantiations.SINGLETON) {
177            singleton = getNewInstance(null, true); // the context is empty since it's not required at this place (no
178                                                    // use - just preparing)
179        }
180    }
181
182    /**
183     * Get the Java class and generic type managed by this marshaller. If not found, search in the parent.
184     *
185     * @param clazz The marshaller class to analyse.
186     * @since 7.2
187     */
188    private void loadMarshalledType(Class<?> clazz) {
189        if (isWriter() || isReader()) {
190            Map<TypeVariable<?>, Type> typeArguments = TypeUtils.getTypeArguments(clazz, Marshaller.class);
191            for (Map.Entry<TypeVariable<?>, Type> entry : typeArguments.entrySet()) {
192                if (Marshaller.class.equals(entry.getKey().getGenericDeclaration())) {
193                    genericType = TypeUtils.unrollVariables(typeArguments, entry.getValue());
194                    marshalledType = TypeUtils.getRawType(genericType, null);
195                    break;
196                }
197            }
198        }
199    }
200
201    /**
202     * Get the first found {@link Setup} annotation in the class hierarchy. If not found in the given class, search in
203     * the parent.
204     *
205     * @param clazz The class to analyse.
206     * @return The first found {@link Setup} annotation.
207     * @since 7.2
208     */
209    private Setup loadSetup(Class<?> clazz) {
210        if (Object.class.equals(clazz)) {
211            return null;
212        }
213        return clazz.getAnnotation(Setup.class);
214    }
215
216    /**
217     * Get the first found {@link Supports} annotation in the class hierarchy. If not found in the given class, search
218     * in the parent.
219     *
220     * @param clazz The class to analyse.
221     * @return The first found {@link Supports} annotation.
222     * @since 7.2
223     */
224    private Supports loadSupports(Class<?> clazz) {
225        if (Object.class.equals(clazz)) {
226            return null;
227        }
228        Supports supports = clazz.getAnnotation(Supports.class);
229        if (supports != null) {
230            return supports;
231        } else {
232            return loadSupports(clazz.getSuperclass());
233        }
234    }
235
236    /**
237     * Load every properties that require injection (context and Nuxeo service). Search in the given class and recurse
238     * in the parent class.
239     *
240     * @param clazz The class to analyse.
241     * @since 7.2
242     */
243    private void loadInjections(Class<?> clazz) {
244        if (Object.class.equals(clazz)) {
245            return;
246        }
247        for (Field field : clazz.getDeclaredFields()) {
248            if (field.isAnnotationPresent(Inject.class)) {
249                if (RenderingContext.class.equals(field.getType())) {
250                    field.setAccessible(true);
251                    contextFields.add(field);
252                } else {
253                    field.setAccessible(true);
254                    serviceFields.add(field);
255                }
256            }
257        }
258        loadInjections(clazz.getSuperclass());
259    }
260
261    /**
262     * Create an instance of this marshaller. Depending on the instantiation mode, get the current singleton instance,
263     * get a thread local one or create a new one.
264     *
265     * @param ctx The {@link RenderingContext} to inject, if null create an empty context.
266     * @return An instance of this class.
267     * @since 7.2
268     */
269    @SuppressWarnings("unchecked")
270    public <T> T getInstance(RenderingContext ctx) {
271        RenderingContext realCtx = getRealContext(ctx);
272        switch (instantiation) {
273        case SINGLETON:
274            return (T) getSingletonInstance(realCtx);
275        case PER_THREAD:
276            return (T) getThreadInstance(realCtx);
277        case EACH_TIME:
278            return (T) getNewInstance(realCtx, false);
279        default:
280            throw new NuxeoException("unable to create a marshaller instance for clazz " + clazz.getName());
281        }
282    }
283
284    /**
285     * Get the real context implementation from the given one. If it's a {@link ThreadSafeRenderingContext}, gets the
286     * enclosing one. If the given context is null, create an empty context.
287     *
288     * @param ctx The {@link RenderingContext} from which we want to search for a real context.
289     * @return A {@link RenderingContextImpl}.
290     * @since 7.2
291     */
292    private RenderingContext getRealContext(RenderingContext ctx) {
293        if (ctx == null) {
294            return RenderingContext.CtxBuilder.get();
295        }
296        if (ctx instanceof RenderingContextImpl) {
297            return ctx;
298        }
299        if (ctx instanceof ThreadSafeRenderingContext) {
300            RenderingContext delegate = ((ThreadSafeRenderingContext) ctx).getDelegate();
301            return getRealContext(delegate);
302        }
303        return null;
304    }
305
306    /**
307     * Create or get a singleton instance of the marshaller.
308     *
309     * @param ctx The {@link RenderingContext} to inject.
310     * @return An instance of the marshaller.
311     * @since 7.2
312     */
313    private Object getSingletonInstance(RenderingContext ctx) {
314        if (!servicesInjected) {
315            synchronized (this) {
316                if (!servicesInjected) {
317                    injectServices(singleton);
318                    servicesInjected = true;
319                }
320            }
321        }
322        for (Field contextField : contextFields) {
323            ThreadSafeRenderingContext value;
324            try {
325                value = (ThreadSafeRenderingContext) contextField.get(singleton);
326            } catch (IllegalArgumentException | IllegalAccessException e) {
327                throw new NuxeoException("unable to create a marshaller instance for clazz " + clazz.getName(), e);
328            }
329            value.configureThread(ctx);
330        }
331        return singleton;
332    }
333
334    /**
335     * Create or get a thread local instance of the marshaller.
336     *
337     * @param ctx The {@link RenderingContext} to inject.
338     * @return An instance of the marshaller.
339     * @since 7.2
340     */
341    private Object getThreadInstance(RenderingContext ctx) {
342        if (threadInstance == null) {
343            threadInstance = new ThreadLocal<>();
344        }
345        Object instance = threadInstance.get();
346        if (instance == null) {
347            instance = getNewInstance(ctx, false);
348            threadInstance.set(instance);
349        } else {
350            for (Field contextField : contextFields) {
351                try {
352                    contextField.set(instance, ctx);
353                } catch (IllegalArgumentException | IllegalAccessException e) {
354                    throw new NuxeoException("unable to create a marshaller instance for clazz " + clazz.getName(), e);
355                }
356            }
357        }
358        return instance;
359    }
360
361    /**
362     * Create a new instance of the marshaller. It injects the required services if the marshaller is not a singleton.
363     * If it's a singleton, it prepares the context variables to handle thread localized context. Then it injects the
364     * given ctx.
365     *
366     * @param ctx The {@link RenderingContext} to inject.
367     * @return An instance of the marshaller.
368     * @since 7.2
369     */
370    public Object getNewInstance(RenderingContext ctx, boolean singleton) {
371        try {
372            Object instance = clazz.getDeclaredConstructor().newInstance();
373            if (!singleton) {
374                // inject services right now - do not for the singleton
375                injectServices(instance);
376            }
377            injectCtx(instance, ctx, singleton);
378            return instance;
379        } catch (ReflectiveOperationException e) {
380            throw new NuxeoException("unable to create a marshaller instance for clazz " + clazz.getName(), e);
381        }
382    }
383
384    /**
385     * Inject the context.
386     */
387    public void injectCtx(Object instance, RenderingContext ctx, boolean singleton) {
388        try {
389            for (Field contextField : contextFields) {
390                if (singleton) {
391                    ThreadSafeRenderingContext safeCtx = new ThreadSafeRenderingContext();
392                    safeCtx.configureThread(ctx);
393                    contextField.set(instance, safeCtx);
394                } else {
395                    contextField.set(instance, ctx);
396                }
397            }
398        } catch (IllegalArgumentException | IllegalAccessException e) {
399            throw new NuxeoException("unable to inject the ctx in the marshaller instance for clazz " + clazz.getName(),
400                    e);
401        }
402    }
403
404    /**
405     * Inject the services.
406     */
407    public void injectServices(Object instance) {
408        try {
409            for (Field serviceField : serviceFields) {
410                Object service = Framework.getService(serviceField.getType());
411                if (service == null) {
412                    throw new NuxeoException("unable to inject a service " + serviceField.getType().getName()
413                            + " in the marshaller clazz " + clazz.getName());
414                }
415                serviceField.set(instance, service);
416            }
417        } catch (IllegalArgumentException | IllegalAccessException e) {
418            throw new NuxeoException(
419                    "unable to inject the services in the marshaller instance for clazz " + clazz.getName(), e);
420        }
421    }
422
423    public Instantiations getInstantiations() {
424        return instantiation;
425    }
426
427    public Integer getPriority() {
428        return priority;
429    }
430
431    public List<MediaType> getSupports() {
432        return supports;
433    }
434
435    public Class<?> getMarshalledType() {
436        return marshalledType;
437    }
438
439    public Type getGenericType() {
440        return genericType;
441    }
442
443    public boolean isMarshaller() {
444        return Marshaller.class.isAssignableFrom(clazz);
445    }
446
447    public boolean isWriter() {
448        return Writer.class.isAssignableFrom(clazz);
449    }
450
451    public boolean isReader() {
452        return Reader.class.isAssignableFrom(clazz);
453    }
454
455    @Override
456    public int compareTo(MarshallerInspector inspector) {
457        if (inspector != null) {
458            // compare priorities
459            int result = getPriority().compareTo(inspector.getPriority());
460            if (result != 0) {
461                return -result;
462            }
463            // then, compare instantiation mode: singleton > thread > each
464            result = getInstantiations().compareTo(inspector.getInstantiations());
465            if (result != 0) {
466                return -result;
467            }
468            // specialize marshaller are preferred: managed class IntegerProperty > AbstractProperty > Property
469            if (isMarshaller() && inspector.isMarshaller()) {
470                if (!getMarshalledType().equals(inspector.getMarshalledType())) {
471                    if (getMarshalledType().isAssignableFrom(inspector.getMarshalledType())) {
472                        return 1;
473                    } else if (inspector.getMarshalledType().isAssignableFrom(getMarshalledType())) {
474                        return -1;
475                    }
476                }
477            }
478            // force sub classes to manage their priorities: StandardWriter > CustomWriter extends StandardWriter
479            // let the reference implementations priority
480            if (!clazz.equals(inspector.clazz)) {
481                if (clazz.isAssignableFrom(inspector.clazz)) {
482                    return -1;
483                } else if (inspector.clazz.isAssignableFrom(clazz)) {
484                    return 1;
485                }
486            }
487            // This is just optimization :
488            // priorise DocumentModel, Property
489            // then NuxeoPrincipal, NuxeoGroup and List<DocumentModel>
490            if ((isWriter() && inspector.isWriter()) || (isReader() && inspector.isReader())) {
491                boolean mineIsTop = isTopPriority(genericType);
492                boolean thatIsTop = isTopPriority(inspector.genericType);
493                if (mineIsTop && !thatIsTop) {
494                    return -1;
495                } else if (!mineIsTop && thatIsTop) {
496                    return 1;
497                }
498                boolean mineIsBig = isBigPriority(genericType);
499                boolean thatIsBig = isBigPriority(inspector.genericType);
500                if (mineIsBig && !thatIsBig) {
501                    return -1;
502                } else if (!mineIsBig && thatIsBig) {
503                    return 1;
504                }
505            }
506            return -clazz.getName().compareTo(inspector.clazz.getName());
507        }
508        return 1;
509    }
510
511    private static boolean isTopPriority(Type type) {
512        return TypeUtils.isAssignable(type, DocumentModel.class) || TypeUtils.isAssignable(type, Property.class);
513    }
514
515    private static boolean isBigPriority(Type type) {
516        return TypeUtils.isAssignable(type, NuxeoPrincipal.class) || TypeUtils.isAssignable(type, NuxeoGroup.class)
517                || TypeUtils.isAssignable(type, TypeUtils.parameterize(List.class, DocumentModel.class));
518    }
519
520    @Override
521    public int hashCode() {
522        final int prime = 31;
523        int result = 1;
524        result = prime * result + ((clazz == null) ? 0 : clazz.hashCode());
525        return result;
526    }
527
528    /**
529     * Two {@link MarshallerInspector} are equals if their managed clazz are the same.
530     */
531    @Override
532    public boolean equals(Object obj) {
533        if (this == obj) {
534            return true;
535        }
536        if (obj == null) {
537            return false;
538        }
539        if (!(obj instanceof MarshallerInspector)) {
540            return false;
541        }
542        MarshallerInspector other = (MarshallerInspector) obj;
543        return clazz.equals(other.clazz);
544    }
545
546
547
548}