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