/* * Copyright 2008-2014 the original author or authors * * 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. */ package org.kaleidofoundry.core.context; import static org.kaleidofoundry.core.util.AspectjHelper.debugJoinPoint; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.FieldSignature; import org.kaleidofoundry.core.lang.annotation.Task; import org.kaleidofoundry.core.lang.annotation.TaskLabel; import org.kaleidofoundry.core.plugin.PluginHelper; import org.kaleidofoundry.core.plugin.model.Plugin; import org.kaleidofoundry.core.util.ReflectionHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This aspect is used to inject {@link RuntimeContext} on a field annotated by {@link Context} * <p> * <b>Usage 1 :</b><br/> * <ol> * <li>A class field {@link RuntimeContext} is annotated {@link Context},</li> * <li>This aspect have to init this field, using {@link Context} annotation meta data,</li> * <li>See method poincut {@link #trackRuntimeContextField(JoinPoint, org.aspectj.lang.JoinPoint.EnclosingStaticPart)}</li> * <li>Example : * * <pre> * public class MyClass { * * @Context(name = "jndi.context") * private RuntimeContext<?> context; // no need to instantiate it * * public MyClass() { * ... * // you can use context field in your constructor * System.out.println(context.getString("...")); * ... * } * } * </pre> * * </li> * </ol> * </p> * <p> * <b>Usage 2 :</b><br/> * <ol> * <li>A class field is annotated {@link Context} (and this field is not a {@link RuntimeContext} instance),</li> * <li>The field class have a {@link RuntimeContext} field (which is not annotated {@link Context}),</li> * <li>This aspect have to init this field, using {@link Context} annotation meta data,</li> * <li>Example : * * <pre> * public class MyControler { * * @Context(name = "myService.context") * private MyService myService; // no need to instantiate it * * public void processing(...) { * // use myService with the given context * myService.echo(); * ... * } * } * * public class MyService { * * private RuntimeContext<MySingleService> context; * * public MySingleService() { * ... * // you can use context field in your constructor * System.out.println(context.getString("...")); * ... * } * * public void echo() { * return contex.toString(); * } * * } * * * </pre> * * </li> * </ol> * </p> * * @author jraduget */ @Aspect public class AnnotationContexInjectorAspect { private static final Logger LOGGER = LoggerFactory.getLogger(AnnotationContexInjectorAspect.class); // map of @Context field injected state, used by complex RuntimeContext aggregation private static final ConcurrentMap<Field, Boolean> _$injectedFieldMap = new ConcurrentHashMap<Field, Boolean>(); /** * */ public AnnotationContexInjectorAspect() { LOGGER.debug("@Aspect({}) new instance", getClass().getName()); } // ************************************************************************************************************************************** // Usage 1 : simple RuntimeContext injection // ************************************************************************************************************************************** /** * @param jp * @param esjp * @return <code>true / false</code> to enable poincut */ // no need to filter on field modifier here, otherwise you can use private || !public at first get argument @Pointcut("get(@org.kaleidofoundry.core.context.Context org.kaleidofoundry.core.context.RuntimeContext *) && if()") public static boolean trackRuntimeContextField(final JoinPoint jp, final JoinPoint.EnclosingStaticPart esjp) { LOGGER.debug("@Pointcut({}) trackRuntimeContextField match", AnnotationContexInjectorAspect.class.getName()); return true; } /** * @param jp * @param esjp * @param thisJoinPoint * @param annotation * @return <code>true / false</code> to enable poincut * @throws Throwable */ // track field with ProceedingJoinPoint and annotation information with @annotation(annotation) @SuppressWarnings("unchecked") @Around("trackRuntimeContextField(jp, esjp) && @annotation(annotation)") public Object trackRuntimeContextFieldToInject(final JoinPoint jp, final JoinPoint.EnclosingStaticPart esjp, final ProceedingJoinPoint thisJoinPoint, final Context annotation) throws Throwable { if (thisJoinPoint.getSignature() instanceof FieldSignature) { final FieldSignature fs = (FieldSignature) thisJoinPoint.getSignature(); final Object target = thisJoinPoint.getTarget(); final Field field = fs.getField(); field.setAccessible(true); final Object currentValue = field.get(target); if (currentValue == null) { final RuntimeContext<?> runtimeContext = RuntimeContext.createFrom(annotation, fs.getName(), fs.getFieldType()); field.set(target, runtimeContext); return runtimeContext; } else { return thisJoinPoint.proceed(); } } else { throw new IllegalStateException("aspect advise handle only field, please check your pointcut"); } } // ************************************************************************************************************************************** // Usage 2 : complex RuntimeContext injection // ************************************************************************************************************************************** /** * @param jp * @param esjp * @return <code>true / false</code> to enable poincut */ // no need to filter on field modifier here, otherwise you can use private || !public at first get argument @Pointcut("get(@org.kaleidofoundry.core.context.Context !org.kaleidofoundry.core.context.RuntimeContext *) && if()") public static boolean trackAgregatedRuntimeContextField(final JoinPoint jp, final JoinPoint.EnclosingStaticPart esjp) { LOGGER.debug("@Pointcut({}) trackAgregatedRuntimeContextField match", AnnotationContexInjectorAspect.class.getName()); return true; } /** * @param jp * @param esjp * @param thisJoinPoint * @param annotation * @return <code>true / false</code> if join point have been processed * @throws Throwable */ // track field with ProceedingJoinPoint and annotation information with @annotation(annotation) @Around("trackAgregatedRuntimeContextField(jp, esjp) && @annotation(annotation)") @Task(comment = "check and handle reflection exception ", labels = TaskLabel.Enhancement) public Object trackAgregatedRuntimeContextFieldToInject(final JoinPoint jp, final JoinPoint.EnclosingStaticPart esjp, final ProceedingJoinPoint thisJoinPoint, final Context annotation) throws Throwable { debugJoinPoint(LOGGER, jp); if (thisJoinPoint.getSignature() instanceof FieldSignature) { final FieldSignature annotatedFieldSignature = (FieldSignature) thisJoinPoint.getSignature(); final Field annotatedField = annotatedFieldSignature.getField(); final Boolean annotatedFieldInjected = _$injectedFieldMap.get(annotatedField); if (LOGGER.isDebugEnabled()) { LOGGER.debug("\t<joinpoint.field.{}.injected>\t{}", annotatedField.getName(), annotatedFieldInjected == null ? Boolean.FALSE : annotatedFieldInjected.booleanValue()); } // does the field to inject have already been injected if (annotatedFieldInjected == null || !annotatedFieldInjected.booleanValue()) { // we need to access to its fields, in order to lookup for a RuntimeContext field annotatedField.setAccessible(true); final Object targetInstance = thisJoinPoint.getTarget(); Object fieldValue = targetInstance != null ? annotatedField.get(targetInstance) : null; // process field initialize if null if (targetInstance != null && fieldValue == null) { // does the plugin interface have a provider specify if (annotatedField.getType().isAnnotationPresent(Provider.class)) { // create provider using annotation meta-information final Provider providerInfo = annotatedField.getType().getAnnotation(Provider.class); final Constructor<? extends ProviderService<?>> providerConstructor = providerInfo.value().getConstructor(Class.class); final ProviderService<?> fieldProviderInstance = providerConstructor.newInstance(annotatedField.getType()); // invoke provides method with Context annotation meta-informations final Method providesMethod = providerInfo.value().getMethod(ProviderService.PROVIDES_METHOD, Context.class, String.class, Class.class); try { fieldValue = providesMethod.invoke(fieldProviderInstance, annotation, annotatedField.getName(), annotatedField.getType()); } catch (final InvocationTargetException ite) { // direct runtime exception like ContextException... throw ite.getCause() != null ? ite.getCause() : (ite.getTargetException() != null ? ite.getTargetException() : ite); } // set the field that was not yet injected annotatedField.set(targetInstance, fieldValue); } } // if field instance have RuntimeContext not annotated @Context, that have not yet been initialize if (targetInstance != null && fieldValue != null) { // does a RuntimeContext field have been found boolean targetRuntimeContextFieldFound = false; // scan accessible field (private, protected, public) final Set<Field> contextFields = ReflectionHelper.getAllDeclaredFields(fieldValue.getClass(), Modifier.PRIVATE, Modifier.PROTECTED, Modifier.PUBLIC); // for each RuntimeContext field, that are not annotated by @Context (to skip direct injection aspect) for (final Field cfield : contextFields) { if (cfield.getType().isAssignableFrom(RuntimeContext.class) && !cfield.isAnnotationPresent(Context.class)) { RuntimeContext<?> currentRuntimeContext = null; targetRuntimeContextFieldFound = true; cfield.setAccessible(true); currentRuntimeContext = (RuntimeContext<?>) cfield.get(fieldValue); // if runtime context field have not yet been set if (currentRuntimeContext == null || !currentRuntimeContext.hasBeenInjectedByAnnotationProcessing()) { // does the implementation is a declare plugin final Plugin<?> interfacePlugin = PluginHelper.getInterfacePlugin(fieldValue); // create a new instance of RuntimeContext<?>, using annotation information final RuntimeContext<?> newRuntimeContext = RuntimeContext.createFrom(annotation, annotatedField.getName(), interfacePlugin != null ? interfacePlugin.getAnnotatedClass() : null); // inject new RuntimeContext<?> field instance to the target instance if (!Modifier.isFinal(cfield.getModifiers())) { cfield.set(fieldValue, newRuntimeContext); } // re-copy runtimeContext information to the existing one else { if (currentRuntimeContext != null) { RuntimeContext.copyFrom(newRuntimeContext, currentRuntimeContext); } else { // RuntimeContext field is final && null throw new ContextException("context.annotation.illegalfield", fieldValue.getClass().getName(), cfield.getName()); } } // mark instance has injected _$injectedFieldMap.put(annotatedField, Boolean.TRUE); } } } // coherence checks if (!targetRuntimeContextFieldFound) { throw new ContextException("context.annotation.illegaluse.noRuntimeContextField", annotatedFieldSignature .getFieldType().getName() + "#" + annotatedField.getName(), Context.class.getName()); } } } return thisJoinPoint.proceed(); } else { throw new IllegalStateException("aspect advise handle only field, please check your pointcut"); } } }