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