/*
* (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Nicolas Chapurlat <nchapurlat@nuxeo.com>
*/
package org.nuxeo.ecm.core.io.registry.reflect;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.ws.rs.core.MediaType;
import org.apache.commons.lang3.reflect.TypeUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.NuxeoGroup;
import org.nuxeo.ecm.core.api.NuxeoPrincipal;
import org.nuxeo.ecm.core.api.model.Property;
import org.nuxeo.ecm.core.io.registry.Marshaller;
import org.nuxeo.ecm.core.io.registry.MarshallerRegistry;
import org.nuxeo.ecm.core.io.registry.MarshallingException;
import org.nuxeo.ecm.core.io.registry.Reader;
import org.nuxeo.ecm.core.io.registry.Writer;
import org.nuxeo.ecm.core.io.registry.context.RenderingContext;
import org.nuxeo.ecm.core.io.registry.context.RenderingContextImpl;
import org.nuxeo.ecm.core.io.registry.context.ThreadSafeRenderingContext;
import org.nuxeo.runtime.api.Framework;
/**
* Utility class used to instanciate marshallers. This class checks if a marshaller has annotation {@link Setup} and
* inspect every attributes having {@link Inject} annotation.
* <p>
* To get a valid marshaller instance :
* <ul>
* <li>Create an inspector for your marshaller class using constructor {@link #MarshallerInspector(Class)}. This will
* checks your marshaller has annotation {@link Setup} and inspect every attributes having {@link Inject}
* annotation.</li>
* <li>You can check it's a valid marshaller by calling @ #isValid()}</li>
* <li>You can check it's a {@link Writer} by calling {@link #isWriter()}</li>
* <li>You can check it's a {@link Reader} by calling {@link #isReader()}</li>
* <li>You can finally call {@link #getInstance(RenderingContext)} to get a valid marshaller instance with
* {@link RenderingContext} and required services injected.</li>
* </ul>
* </p>
* <p>
* This class implements {@link Comparable} and then handle marshaller priorities rules: look at
* {@link MarshallerRegistry} javadoc to read the rules.
* </p>
*
* @since 7.2
*/
public class MarshallerInspector implements Comparable<MarshallerInspector> {
private static final Log log = LogFactory.getLog(MarshallerInspector.class);
private Class<?> clazz;
private Integer priority;
private Instantiations instantiation;
private List<MediaType> supports = new ArrayList<MediaType>();
private Constructor<?> constructor;
private List<Field> serviceFields = new ArrayList<Field>();
private List<Field> contextFields = new ArrayList<Field>();
private Object singleton;
/**
* A boolean to save the service instrumentation state
*/
private volatile boolean servicesInjected;
private ThreadLocal<Object> threadInstance;
private Class<?> marshalledType;
private Type genericType;
/**
* Create an inspector for the given class.
*
* @param clazz The class to analyse and instantiate.
*/
public MarshallerInspector(Class<?> clazz) {
this.clazz = clazz;
load();
}
/**
* Introspect this marshaller: gets instantiation mode, supported mimetype, gets the managed class, generic type and
* load every needed injection to be ready to create an instance quickly.
*
* @since 7.2
*/
private void load() {
// checks if there's a public constructor without parameters
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
if (Modifier.isPublic(constructor.getModifiers()) && constructor.getParameterTypes().length == 0) {
this.constructor = constructor;
break;
}
}
if (constructor == null) {
throw new MarshallingException("No public constructor found for class " + clazz.getName()
+ ". Instanciation will not be possible.");
}
// load instantiation mode
Setup setup = loadSetup(clazz);
if (setup == null) {
throw new MarshallingException("No required @Setup annotation found for class " + clazz.getName()
+ ". Instanciation will not be possible.");
}
if (!isReader() && !isWriter()) {
throw new MarshallingException(
"MarshallerInspector only supports Reader and Writer: you must implement one of this interface for this class: "
+ clazz.getName());
}
if (isReader() && isWriter()) {
throw new MarshallingException(
"MarshallerInspector only supports either Reader or Writer: you must implement only one of this interface: "
+ clazz.getName());
}
instantiation = setup.mode();
priority = setup.priority();
// load supported mimetype
Supports supports = loadSupports(clazz);
if (supports != null) {
for (String mimetype : supports.value()) {
try {
MediaType mediaType = MediaType.valueOf(mimetype);
this.supports.add(mediaType);
} catch (IllegalArgumentException e) {
log.warn("In marshaller class " + clazz.getName() + ", the declared mediatype " + mimetype
+ " cannot be parsed as a mimetype");
}
}
}
if (this.supports.isEmpty()) {
log.warn("The marshaller " + clazz.getName()
+ " does not support any mimetype. You can add some using annotation @Supports");
}
// loads the marshalled type and generic type
loadMarshalledType(clazz);
// load properties that require injection
loadInjections(clazz);
// warn if several context found
if (contextFields.size() > 1) {
log.warn("The marshaller " + clazz.getName()
+ " has more than one context injected property. You probably should use a context from a parent class.");
}
if (instantiation == Instantiations.SINGLETON) {
singleton = getNewInstance(null, true); // the context is empty since it's not required at this place (no
// use - just preparing)
}
}
/**
* Get the Java class and generic type managed by this marshaller. If not found, search in the parent.
*
* @param clazz The marshaller class to analyse.
* @since 7.2
*/
private void loadMarshalledType(Class<?> clazz) {
if (isWriter() || isReader()) {
Map<TypeVariable<?>, Type> typeArguments = TypeUtils.getTypeArguments(clazz, Marshaller.class);
for (Map.Entry<TypeVariable<?>, Type> entry : typeArguments.entrySet()) {
if (Marshaller.class.equals(entry.getKey().getGenericDeclaration())) {
genericType = TypeUtils.unrollVariables(typeArguments, entry.getValue());
marshalledType = TypeUtils.getRawType(genericType, null);
break;
}
}
}
}
/**
* Get the first found {@link Setup} annotation in the class hierarchy. If not found in the given class, search in
* the parent.
*
* @param clazz The class to analyse.
* @return The first found {@link Setup} annotation.
* @since 7.2
*/
private Setup loadSetup(Class<?> clazz) {
if (Object.class.equals(clazz)) {
return null;
}
return clazz.getAnnotation(Setup.class);
}
/**
* Get the first found {@link Supports} annotation in the class hierarchy. If not found in the given class, search
* in the parent.
*
* @param clazz The class to analyse.
* @return The first found {@link Supports} annotation.
* @since 7.2
*/
private Supports loadSupports(Class<?> clazz) {
if (Object.class.equals(clazz)) {
return null;
}
Supports supports = clazz.getAnnotation(Supports.class);
if (supports != null) {
return supports;
} else {
return loadSupports(clazz.getSuperclass());
}
}
/**
* Load every properties that require injection (context and Nuxeo service). Search in the given class and recurse
* in the parent class.
*
* @param clazz The class to analyse.
* @since 7.2
*/
private void loadInjections(Class<?> clazz) {
if (Object.class.equals(clazz)) {
return;
}
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class)) {
if (RenderingContext.class.equals(field.getType())) {
field.setAccessible(true);
contextFields.add(field);
} else {
field.setAccessible(true);
serviceFields.add(field);
}
}
}
loadInjections(clazz.getSuperclass());
}
/**
* Create an instance of this marshaller. Depending on the instantiation mode, get the current singleton instance,
* get a thread local one or create a new one.
*
* @param ctx The {@link RenderingContext} to inject, if null create an empty context.
* @return An instance of this class.
* @since 7.2
*/
@SuppressWarnings("unchecked")
public <T> T getInstance(RenderingContext ctx) {
RenderingContext realCtx = getRealContext(ctx);
switch (instantiation) {
case SINGLETON:
return (T) getSingletonInstance(realCtx);
case PER_THREAD:
return (T) getThreadInstance(realCtx);
case EACH_TIME:
return (T) getNewInstance(realCtx, false);
default:
throw new NuxeoException("unable to create a marshaller instance for clazz " + clazz.getName());
}
}
/**
* Get the real context implementation from the given one. If it's a {@link ThreadSafeRenderingContext}, gets the
* enclosing one. If the given context is null, create an empty context.
*
* @param ctx The {@link RenderingContext} from which we want to search for a real context.
* @return A {@link RenderingContextImpl}.
* @since 7.2
*/
private RenderingContext getRealContext(RenderingContext ctx) {
if (ctx == null) {
return RenderingContext.CtxBuilder.get();
}
if (ctx instanceof RenderingContextImpl) {
return ctx;
}
if (ctx instanceof ThreadSafeRenderingContext) {
RenderingContext delegate = ((ThreadSafeRenderingContext) ctx).getDelegate();
return getRealContext(delegate);
}
return null;
}
/**
* Create or get a singleton instance of the marshaller.
*
* @param ctx The {@link RenderingContext} to inject.
* @return An instance of the marshaller.
* @since 7.2
*/
private Object getSingletonInstance(RenderingContext ctx) {
if (!servicesInjected) {
synchronized (this) {
if (!servicesInjected) {
injectServices(singleton);
servicesInjected = true;
}
}
}
for (Field contextField : contextFields) {
ThreadSafeRenderingContext value;
try {
value = (ThreadSafeRenderingContext) contextField.get(singleton);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new NuxeoException("unable to create a marshaller instance for clazz " + clazz.getName(), e);
}
value.configureThread(ctx);
}
return singleton;
}
/**
* Create or get a thread local instance of the marshaller.
*
* @param ctx The {@link RenderingContext} to inject.
* @return An instance of the marshaller.
* @since 7.2
*/
private Object getThreadInstance(RenderingContext ctx) {
if (threadInstance == null) {
threadInstance = new ThreadLocal<Object>();
}
Object instance = threadInstance.get();
if (instance == null) {
instance = getNewInstance(ctx, false);
threadInstance.set(instance);
} else {
for (Field contextField : contextFields) {
try {
contextField.set(instance, ctx);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new NuxeoException("unable to create a marshaller instance for clazz " + clazz.getName(), e);
}
}
}
return instance;
}
/**
* Create a new instance of the marshaller. It injects the required services if the marshaller is not a singleton.
* If it's a singleton, it prepares the context variables to handle thread localized context. Then it injects the
* given ctx.
*
* @param ctx The {@link RenderingContext} to inject.
* @return An instance of the marshaller.
* @since 7.2
*/
public Object getNewInstance(RenderingContext ctx, boolean singleton) {
try {
Object instance = clazz.newInstance();
if (!singleton) {
// inject services right now - do not for the singleton
injectServices(instance);
}
injectCtx(instance, ctx, singleton);
return instance;
} catch (IllegalArgumentException | IllegalAccessException | InstantiationException e) {
throw new NuxeoException("unable to create a marshaller instance for clazz " + clazz.getName(), e);
}
}
/**
* Inject the context.
*/
public void injectCtx(Object instance, RenderingContext ctx, boolean singleton) {
try {
for (Field contextField : contextFields) {
if (singleton) {
ThreadSafeRenderingContext safeCtx = new ThreadSafeRenderingContext();
safeCtx.configureThread(ctx);
contextField.set(instance, safeCtx);
} else {
contextField.set(instance, ctx);
}
}
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new NuxeoException("unable to inject the ctx in the marshaller instance for clazz " + clazz.getName(),
e);
}
}
/**
* Inject the services.
*/
public void injectServices(Object instance) {
try {
for (Field serviceField : serviceFields) {
Object service = Framework.getService(serviceField.getType());
if (service == null) {
throw new NuxeoException("unable to inject a service " + serviceField.getType().getName()
+ " in the marshaller clazz " + clazz.getName());
}
serviceField.set(instance, service);
}
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new NuxeoException(
"unable to inject the services in the marshaller instance for clazz " + clazz.getName(), e);
}
}
public Instantiations getInstantiations() {
return instantiation;
}
public Integer getPriority() {
return priority;
}
public List<MediaType> getSupports() {
return supports;
}
public Class<?> getMarshalledType() {
return marshalledType;
}
public Type getGenericType() {
return genericType;
}
public boolean isMarshaller() {
return Marshaller.class.isAssignableFrom(clazz);
}
public boolean isWriter() {
return Writer.class.isAssignableFrom(clazz);
}
public boolean isReader() {
return Reader.class.isAssignableFrom(clazz);
}
@Override
public int compareTo(MarshallerInspector inspector) {
if (inspector != null) {
// compare priorities
int result = getPriority().compareTo(inspector.getPriority());
if (result != 0) {
return -result;
}
// then, compare instantiation mode: singleton > thread > each
result = getInstantiations().compareTo(inspector.getInstantiations());
if (result != 0) {
return -result;
}
// specialize marshaller are preferred: managed class IntegerProperty > AbstractProperty > Property
if (isMarshaller() && inspector.isMarshaller()) {
if (!getMarshalledType().equals(inspector.getMarshalledType())) {
if (getMarshalledType().isAssignableFrom(inspector.getMarshalledType())) {
return 1;
} else if (inspector.getMarshalledType().isAssignableFrom(getMarshalledType())) {
return -1;
}
}
}
// force sub classes to manage their priorities: StandardWriter > CustomWriter extends StandardWriter
// let the reference implementations priority
if (!clazz.equals(inspector.clazz)) {
if (clazz.isAssignableFrom(inspector.clazz)) {
return -1;
} else if (inspector.clazz.isAssignableFrom(clazz)) {
return 1;
}
}
// This is just optimization :
// priorise DocumentModel, Property
// then NuxeoPrincipal, NuxeoGroup and List<DocumentModel>
if ((isWriter() && inspector.isWriter()) || (isReader() && inspector.isReader())) {
boolean mineIsTop = isTopPriority(genericType);
boolean thatIsTop = isTopPriority(inspector.genericType);
if (mineIsTop && !thatIsTop) {
return -1;
} else if (!mineIsTop && thatIsTop) {
return 1;
}
boolean mineIsBig = isBigPriority(genericType);
boolean thatIsBig = isBigPriority(inspector.genericType);
if (mineIsBig && !thatIsBig) {
return -1;
} else if (!mineIsBig && thatIsBig) {
return 1;
}
}
return -clazz.getName().compareTo(inspector.clazz.getName());
}
return 1;
}
private static boolean isTopPriority(Type type) {
return TypeUtils.isAssignable(type, DocumentModel.class) || TypeUtils.isAssignable(type, Property.class);
}
private static boolean isBigPriority(Type type) {
return TypeUtils.isAssignable(type, NuxeoPrincipal.class) || TypeUtils.isAssignable(type, NuxeoGroup.class)
|| TypeUtils.isAssignable(type, TypeUtils.parameterize(List.class, DocumentModel.class));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((clazz == null) ? 0 : clazz.hashCode());
return result;
}
/**
* Two {@link MarshallerInspector} are equals if their managed clazz are the same.
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof MarshallerInspector)) {
return false;
}
MarshallerInspector other = (MarshallerInspector) obj;
return clazz.equals(other.clazz);
}
}