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