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