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}