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}