package org.ff4j.aop;
/*
* #%L ff4j-aop %% Copyright (C) 2013 Ff4J %% 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. #L%
*/
import static org.ff4j.utils.MappingUtil.instanceFlippingStrategy;
import static org.ff4j.utils.MappingUtil.toMap;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javax.lang.model.type.NullType;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.ff4j.FF4j;
import org.ff4j.core.FlippingExecutionContext;
import org.ff4j.core.FlippingStrategy;
import org.ff4j.utils.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
/**
* At runtime check presence of annotation @{Flip}, then evaluate if the related feature id is enabled.
* If the feature is enabled, the implementation is route to the correct implementation.
*
* @author Cedrick LUNVEN (@clunven)
*/
@Component("ff.advisor")
public class FeatureAdvisor implements MethodInterceptor {
/** Log with target className. */
private final static Logger LOGGER = LoggerFactory.getLogger(FeatureAdvisor.class);
/** Spring Application Context. */
@Autowired
private ApplicationContext appCtx;
/** Injection of current FF4J bean. */
@Autowired
private FF4j ff4j;
/** {@inheritDoc} */
@Override
public Object invoke(final MethodInvocation mi) throws Throwable {
Flip ff4jAnnotation = getFF4jAnnotation(mi);
// Method is annotated and the related feature is ON
if (ff4jAnnotation != null && check(ff4jAnnotation, mi)) {
// Do we use the alter bean defined in the annotation ?
String alterBean = ff4jAnnotation.alterBean();
if (Util.hasLength(alterBean)
// Bean name exist
& appCtx.containsBean(alterBean)
// Bean name is not the same as current
& !alterBean.equals(getExecutedBeanName(mi))) {
return invokeAlterBean(mi, alterBean);
}
// Or else do we use the alter class defined in the annotation ?
Class<?> alterClazz = ff4jAnnotation.alterClazz();
if (Util.isValidClass(alterClazz)
// Alter class is not the same as current
& (alterClazz != getExecutedClass(mi))) {
return invokeAlterClazz(mi, ff4jAnnotation);
}
}
// No feature toggle (no annotation nor feature OFF)
return mi.proceed();
}
/**
* Call if Flipped based on different parameters of the annotation
*
* @param ff
* annotation over current method
* @param context
* @return if flippinf should be considere
*/
protected boolean check(Flip ff, MethodInvocation mi) {
// Retrieve optional context with ThreadLocal
FlippingExecutionContext context = getFlippingContext(ff, mi);
// Check ff4j
String featureId = ff.name();
if (ff.flippingStrategy() != NullType.class) {
String fsClassName = ff.flippingStrategy().getName();
FlippingStrategy fs = instanceFlippingStrategy(featureId, fsClassName, toMap(ff.flippingInitParams()));
return getFf4j().checkOveridingStrategy(featureId, fs, context);
}
return getFf4j().check(featureId, context);
}
/**
* Pick annotation from method or class.
*
* @param method
* current method
* @return
* the associated annotation
*/
protected Flip getFF4jAnnotation(MethodInvocation mi) {
if (mi.getMethod().isAnnotationPresent(Flip.class)) {
return mi.getMethod().getAnnotation(Flip.class);
}
Class <?> currentInterface = mi.getMethod().getDeclaringClass();
if (currentInterface.isAnnotationPresent(Flip.class)) {
return currentInterface.getAnnotation(Flip.class);
}
Class <?> currentImplementation = getExecutedClass(mi);
if (currentImplementation.isAnnotationPresent(Flip.class)) {
return currentImplementation.getAnnotation(Flip.class);
}
return null;
}
/**
* Retriveve {@link FlippingExecutionContext} from FF4J or as parameter.
*
* @param ff
* current annotation
* @param mi
* invocation
* @return
*/
protected FlippingExecutionContext getFlippingContext(Flip ff, MethodInvocation mi) {
switch (ff.contextLocation()) {
case FF4J:
return getFf4j().getCurrentContext();
case PARAMETER:
// We are looking for the first parameter (not argument!)
// that is an instance of FlippingExecutionContext
int p = 0;
for (Class<?> cls : mi.getMethod().getParameterTypes()) {
if (FlippingExecutionContext.class.isAssignableFrom(cls)) {
return FlippingExecutionContext.class.cast(mi.getArguments()[p]);
}
p++;
}
case NONE:
default: return null;
}
}
/**
* Find current class based on the {@link MethodInvocation} and passing throug AOP Proxies.
*
* @param pMInvoc
* current method invocation
* @return
* current class of raise error if static
*/
protected Class<?> getExecutedClass(MethodInvocation pMInvoc) {
Class<?> executedClass = null;
Object ref = pMInvoc.getThis();
if (ref != null) {
executedClass = AopUtils.getTargetClass(ref);
}
if (executedClass == null) {
throw new IllegalArgumentException("ff4j-aop: Static methods cannot be feature flipped");
}
return executedClass;
}
/**
* Find bean name related to current method invocation.
* @param pMInvoc
* current method invocation
* @return
* bean name related to this method
*/
protected String getExecutedBeanName(MethodInvocation mi) {
Class<?> targetClass = getExecutedClass(mi);
Component component = targetClass.getAnnotation(Component.class);
if (component != null) {
return component.value();
}
Service service = targetClass.getAnnotation(Service.class);
if (service != null) {
return service.value();
}
Repository repo = targetClass.getAnnotation(Repository.class);
if (repo != null) {
return repo.value();
}
// There is no annotation on the bean, still be declared in applicationContext.xml
try {
// Use BeanDefinition names to loop on each bean and fetch target if proxified
for(String beanName : appCtx.getBeanDefinitionNames()) {
Object bean = appCtx.getBean(beanName);
if (AopUtils.isJdkDynamicProxy(bean)) {
bean = ((Advised) bean).getTargetSource().getTarget();
}
if (bean != null && bean.getClass().isAssignableFrom(targetClass)) {
return beanName;
}
}
} catch (Exception e) {
throw new RuntimeException("ff4j-aop: Cannot read bheind proxy target", e);
}
throw new IllegalArgumentException("ff4j-aop: Feature bean must be annotated as a Service or a Component");
}
private IllegalArgumentException makeIllegalArgumentException(String message, Exception exception) {
return new IllegalArgumentException(message, exception);
}
/**
* Invoke another Bean for the current Method.
*
* @param mi
* current method invocation
* @param alterBean
* target bean
* @return
* return of invocation
* @throws Throwable
* erros occured
*/
protected Object invokeAlterBean(final MethodInvocation mi, String alterBeanName) throws Throwable {
Method method = mi.getMethod();
try {
LOGGER.debug("FeatureFlipping on method:{} class:{}", method.getName(), method.getDeclaringClass().getName());
Object alterbean = appCtx.getBean(alterBeanName, method.getDeclaringClass());
return method.invoke(alterbean, mi.getArguments());
} catch (InvocationTargetException invocationTargetException) {
if(!ff4j.isAlterBeanThrowInvocationTargetException() && invocationTargetException.getCause() != null) {
throw invocationTargetException.getCause();
}
throw makeIllegalArgumentException("ff4j-aop: Cannot invoke method " + method.getName() + " on bean " + alterBeanName, invocationTargetException);
} catch (Exception exception) {
throw makeIllegalArgumentException("ff4j-aop: Cannot invoke method " + method.getName() + " on bean " + alterBeanName, exception);
}
}
/**
* Invoke alter class.
*
* @param mi
* method invocation
* @param ff
* ff4j annotation
* @return
* object returned by the
* @throws Throwable
* error during invocation
*/
protected Object invokeAlterClazz(final MethodInvocation mi, Flip ff) throws Throwable {
Class<?> alterClazz = ff.alterClazz();
Method method = mi.getMethod();
Class<?> declaringClass = method.getDeclaringClass();
try {
// Spring context may have a bean of expected type and priority of get instance
for (Object bean : appCtx.getBeansOfType(declaringClass).values()) {
// Correct bean implementing the same class, or proxy of existing class
if (AopUtils.isJdkDynamicProxy(bean) && ((Advised) bean).getTargetSource().getTarget().getClass().equals(alterClazz) ||
AopProxyUtils.ultimateTargetClass(bean).equals(alterClazz)) {
return mi.getMethod().invoke(bean, mi.getArguments());
}
}
// Otherwise instanciate manually
return mi.getMethod().invoke(ff.alterClazz().newInstance(), mi.getArguments());
} catch (IllegalAccessException e) {
throw makeIllegalArgumentException("ff4j-aop: Cannot invoke " + method.getName() + " on alterbean " + declaringClass
+ " please check visibility", e);
} catch (InvocationTargetException invocationTargetException) {
if(!ff4j.isAlterBeanThrowInvocationTargetException() && invocationTargetException.getCause() != null) {
throw invocationTargetException.getCause();
}
throw makeIllegalArgumentException("ff4j-aop: Cannot invoke " + method.getName() + " on alterbean " + declaringClass
+ " please check signatures", invocationTargetException);
} catch (Exception exception) {
throw makeIllegalArgumentException("ff4j-aop: Cannot invoke " + method.getName() + " on alterbean " + declaringClass
+ " please check signatures", exception);
}
}
/**
* Getter accessor for attribute 'ff4j'.
*
* @return current value of 'ff4j'
*/
public FF4j getFf4j() {
return ff4j;
}
/**
* Setter accessor for attribute 'ff4j'.
* @param ff4j
* new value for 'ff4j '
*/
public void setFf4j(FF4j ff4j) {
this.ff4j = ff4j;
}
}