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}