All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.ff4j.aop.FeatureAdvisor Maven / Gradle / Ivy

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 java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;

/**
 * When Proxified, analyze bean to eventually invoke ANOTHER implementation (flip up).
 * 
 * @author Cedrick LUNVEN
 */
@Component("ff.advisor")
public class FeatureAdvisor implements MethodInterceptor, BeanPostProcessor, ApplicationContextAware {

    /** Log with target className. */
    private final Map targetLoggers = new HashMap();

    /** Processed Interfaces. */
    private final Set targetInterfacesNames = new HashSet();

    /** Strategies should be instantiate only once, keep references */
    private final Map strategySingletons = new HashMap();

    /** Spring Application Context. */
    private ApplicationContext appCtx;

    /** Injection of current FF4J bean. */
    @Autowired
    private FF4j ff4j;

    /** {@inheritDoc} */
    @Override
    public Object invoke(final MethodInvocation pMInvoc) throws Throwable {
        Method method = pMInvoc.getMethod();
        Logger targetLogger = getLogger(method);
        Flip ff = null;
        if (method.isAnnotationPresent(Flip.class)) {
            ff = method.getAnnotation(Flip.class);
        } else if (method.getDeclaringClass().isAnnotationPresent(Flip.class)) {
            ff = method.getDeclaringClass().getAnnotation(Flip.class);
        }

        if (ff != null) {
            FlippingExecutionContext context = retrieveContext(ff, pMInvoc, targetLogger);
            if (shouldFlip(ff, context)) {

                // Required parameters
                if (!assertRequiredParams(ff)) {
                    String msg = String.format("alterBeanName or alterClazz is required for {%s}", method.getDeclaringClass());
                    throw new IllegalArgumentException(msg);
                }
                if (shouldCallAlterBeanMethod(pMInvoc, ff.alterBean(), targetLogger)) {
                    return callAlterBeanMethod(pMInvoc, ff.alterBean(), targetLogger);
                }
                // Test alterClazz Property of annotation
                if (shouldCallAlterClazzMethod(pMInvoc, ff.alterClazz(), targetLogger)) {
                    return callAlterClazzMethodOnFirst(pMInvoc, ff, targetLogger);
                }
            }
        }
        // do not catch throwable
        return pMInvoc.proceed();
    }

    private FlippingExecutionContext retrieveContext(Flip ff, MethodInvocation methodInvocation, Logger logger) {
        FlippingExecutionContext context = null;
        if (ff.contextLocation() == ContextLocation.FF4J) {
            context = getFf4j().getCurrentContext();
        } else if (ff.contextLocation() == ContextLocation.PARAMETER) {
            // We are looking for the first parameter (not argument!) that is an instance of FlippingExecutionContext
            int p = 0;
            for (Class cls : methodInvocation.getMethod().getParameterTypes()) {
                if (FlippingExecutionContext.class.isAssignableFrom(cls)) {
                    context = FlippingExecutionContext.class.cast(methodInvocation.getArguments()[p]);
                    break;
                }
                p++;
            }
            if (context == null) {
                logger.warn("ff4j-aop: Context location is 'PARAMETER' and no context was found as parameter" +
                        " (maybe the argument was null)");
            }
        }
        return context;
    }

    /**
     * Check requirements for annoting methods.
     * 
     * @param ff
     *            target annotation {@link Flip}
     * @return is param are correct
     */
    private boolean assertRequiredParams(Flip ff) {
        boolean alterBeanPresent = ff.alterBean() != null && !ff.alterBean().isEmpty();
        boolean alterClazPresent = (ff.alterClazz() != null) && (ff.alterClazz() != NullType.class);
        return alterBeanPresent || alterClazPresent;
    }

    /** {@inheritDoc} */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        // Before Initializing allow to check Annotations
        Class target = bean.getClass();
        // Scan interface only once.
        if (!target.isInterface() && target.getInterfaces() != null) {
            // Get Interface
            for (Class currentInterface : target.getInterfaces()) {
                String currentInterfaceName = currentInterface.getCanonicalName();
                if (!currentInterfaceName.startsWith("java.") && !targetInterfacesNames.contains(currentInterfaceName)) {
                    targetInterfacesNames.add(currentInterfaceName);
                }
            }
        }
        return bean;
    }

    /** {@inheritDoc} */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        return bean;
    }

    /**
     * Store single instance of loggers but init if does not exist
     * 
     * @param targetMethod
     *            current processed method
     * @return singleton for class related to this execution
     */
    private Logger getLogger(Method targetMethod) {
        String methodName = targetMethod.getDeclaringClass().getCanonicalName();
        // Register logger if require
        if (!targetLoggers.containsKey(methodName)) {
            targetLoggers.put(methodName, LoggerFactory.getLogger(targetMethod.getDeclaringClass()));
        }
        return targetLoggers.get(methodName);
    }

    /**
     * Call if Flipped based on different parameters of the annotation
     * 
     * @param ff
     *            annotation over current method
     * @param context
     * @return if flippinf should be considere
     */
    private boolean shouldFlip(Flip ff, FlippingExecutionContext context) {
        boolean shouldFlip;
        if (ff.strategy() != NullType.class) {
            // Does this strategy has already be invoked ?
            String strategyClassName = ff.strategy().getCanonicalName();
            if (!strategySingletons.containsKey(strategyClassName)) {
                try {
                    strategySingletons.put(strategyClassName, (FlippingStrategy) ff.strategy().newInstance());
                } catch (InstantiationException e) {
                    throw new IllegalArgumentException("ff4j-aop: Cannot instantiate alterbean " + strategyClassName
                            + " please check default constructor existence & visibility", e);
                } catch (IllegalAccessException e) {
                    throw new IllegalArgumentException("ff4j-aop: Cannot instantiate alterbean " + strategyClassName
                            + " please check constructor visibility", e);
                }
            }
            FlippingStrategy targetStrategy = strategySingletons.get(strategyClassName);
            shouldFlip = getFf4j().checkOveridingStrategy(ff.name(), targetStrategy, context);
        } else {
            // no strategy, simple flip
            shouldFlip = getFf4j().check(ff.name(), context);
        }
        return shouldFlip;
    }

    /**
     * Flip with alterBean is realized only if 'alterBean' property is filled and valid.
     * 
     * @param pMInvoc
     *            current method invocation
     * @param alterBean
     *            target bean to call
     * @param logger
     *            current logger for the class
     * @return flag if alterBean should be invoked
     */
    private boolean shouldCallAlterBeanMethod(final MethodInvocation pMInvoc, String alterBean, Logger logger) {
        boolean callAlterBeanMethod = false;
        Method method = pMInvoc.getMethod();

        String currentBeanName = currentBeanName(pMInvoc);
        if (alterBean != null && !alterBean.isEmpty()) {
            if (alterBean.equals(currentBeanName)) {
                logger.debug("FeatureFlipping on method:{} class:{} already on the alterBean {}", method.getName(), method
                        .getDeclaringClass().getName(), alterBean);
            } else {
                if (!appCtx.containsBean(alterBean)) {
                    throw new BeanCreationException("ff4j-aop : bean name '" + alterBean
                            + "' has not been found in applicationContext still declared in 'alterBean' property of bean "
                            + method.getDeclaringClass());
                }
                callAlterBeanMethod = true;
            }
        }
        return callAlterBeanMethod;
    }

    private String currentBeanName(MethodInvocation pMInvoc) {
        Class targetClass = pMInvoc.getThis() != null ? AopUtils.getTargetClass(pMInvoc.getThis()) : null;
        if (targetClass == null) {
            throw new IllegalArgumentException("ff4j-aop: Static methods cannot be feature flipped");
        }
        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 Object callAlterBeanMethod(final MethodInvocation pMInvoc, String alterBean, Logger targetLogger) throws Throwable {
        Method method = pMInvoc.getMethod();
        targetLogger.debug("FeatureFlipping on method:{} class:{}", method.getName(), method.getDeclaringClass().getName());
        // invoke same method (interface) with another spring bean (ff.alterBean())
        try {
            return method.invoke(appCtx.getBean(alterBean, method.getDeclaringClass()), pMInvoc.getArguments());
        } catch (InvocationTargetException invocationTargetException) {
            if(!ff4j.isAlterBeanThrowInvocationTargetException() && invocationTargetException.getCause() != null) {
                throw invocationTargetException.getCause();
            }
            throw makeIllegalArgumentException("ff4j-aop: Cannot invoke method " + method.getName() + " on bean " + alterBean, invocationTargetException);
        } catch (Exception exception) {
            throw makeIllegalArgumentException("ff4j-aop: Cannot invoke method " + method.getName() + " on bean " + alterBean, exception);
        }
    }

    private boolean shouldCallAlterClazzMethod(final MethodInvocation pMInvoc, Class alterClass, Logger logger) {
        boolean callAlterBeanMethod = false;
        Method method = pMInvoc.getMethod();
        Class currentClass = pMInvoc.getThis().getClass();
        if (alterClass != null && (alterClass != NullType.class)) {
            callAlterBeanMethod = !currentClass.equals(alterClass);
            if (!callAlterBeanMethod) {
                logger.debug("FeatureFlipping on method:{} class:{} already on the alterClazz {}", method.getName(), method
                        .getDeclaringClass().getName(), alterClass);
            }
        }
        return callAlterBeanMethod;
    }

    private Object callAlterClazzMethodOnFirst(final MethodInvocation pMInvoc, Flip ff, Logger targetLogger) throws Throwable {
        Map beans = appCtx.getBeansOfType(pMInvoc.getMethod().getDeclaringClass());
        for (Object bean : beans.values()) {
            if (isBeanAProxyOfAlterClass(bean, ff.alterClazz())) {
                return callAlterClazzMethod(pMInvoc, bean, targetLogger);
            }
        }
        throw new BeanCreationException("ff4j-aop : bean with class '" + ff.alterClazz()
                + "' has not been found in applicationContext still declared in 'alterClazz' property of bean "
                + pMInvoc.getMethod().getDeclaringClass());
    }

    private Object callAlterClazzMethod(final MethodInvocation pMInvoc, Object targetBean, Logger targetLogger) throws Throwable {
        Method method = pMInvoc.getMethod();
        String declaringClass = method.getDeclaringClass().getName();
        targetLogger.debug("FeatureFlipping on method:{} class:{}", method.getName(), declaringClass);
        try {
            return method.invoke(targetBean, pMInvoc.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);
        }
    }

    private IllegalArgumentException makeIllegalArgumentException(String message, Exception exception) {
        return new IllegalArgumentException(message, exception);
    }

    protected boolean isBeanAProxyOfAlterClass(Object proxy, Class alterClass) {
        if (AopUtils.isJdkDynamicProxy(proxy)) {
            try {
                return ((Advised) proxy).getTargetSource().getTarget().getClass().equals(alterClass);
            } catch (Exception e) {
                throw new IllegalArgumentException("ff4j-aop: Cannot evaluate is target bean is proxy", e);
            }
        } else {
            // expected to be cglib proxy then, which is simply a specialized class
            return proxy.getClass().equals(alterClass);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.appCtx = applicationContext;
    }

    /**
     * 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;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy