Java tutorial
/* * Copyright 2012 Tacit Knowledge. * * 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 com.tacitknowledge.flip.aspectj; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.tacitknowledge.flip.FeatureService; import com.tacitknowledge.flip.FlipContext; import com.tacitknowledge.flip.aspectj.converters.Converter; import com.tacitknowledge.flip.aspectj.converters.ConvertersHandler; import com.tacitknowledge.flip.model.FeatureState; 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.MethodSignature; /** * The base abstract Aspect class which applies the interception of all * methods marked with {@link Flippable} annotation. If the method is marked with * this annotation will act in the feature toggling. If the feature declared in the * annotation is disabled the {@link Flippable#disabledValue() } will be applied. * If the class itself is marked with {@link Flippable} annotation, then all * methods will act in the feature toggling. Also this aspect allows overriding * the methods parameters which should be marked with {@link FlipParam} annotation. * * @author Serghei Soloviov <ssoloviov@tacitknowledge.com> */ @Aspect public abstract class FlipAbstractAspect { /** * The prepared regular expression to extract value expression from a string. */ private static final Pattern VALUE_EXPRESSION_REGEX = Pattern.compile("\\$\\{([^}]+)\\}"); /** * The default value used if {@link Flippable} annotation has not declared the * {@link Flippable#disabledValue() }. */ private String defaultValue; /** * Returns the {@link FeatureService} object used to calculate the features. * By default it returns the object set in {@link FlipContext}. * * @return {@link FeatureService} object. */ public FeatureService getFeatureService() { return FlipContext.chooseFeatureService(); } /** * Returns the {@link ConvertersHandler} instance used to manage converters. * If the value of {@link Flippable#disabledValue() } or {@link FlipParam#disabledValue() } * properties do not contains a value expression the converter is used to * obtain the required object. * * @return {@link ConvertersHandler} instance. */ public ConvertersHandler getConvertersHandler() { return FlipAopContext.getConvertersHandler(); } /** * Returns the value expressions evaluator. This evaluator is used to evaluate * expressions used in {@link Flippable#disabledValue() } or {@link FlipParam#disabledValue() }. * If these properties contains an expression like this <code>${var}</code> * this value expression evaluator will be used. * * @return {@link ValueExpressionEvaluator} instance. */ public ValueExpressionEvaluator getValueExpressionEvaluator() { return FlipAopContext.getValueExpressionEvaluator(); } /** * Returns the default value if there is no {@link Flippable#disabledValue() } specified. * * @return the default disabled value. */ public String getDefaultValue() { return defaultValue; } /** * Sets the default disabled value. This value will be used if no {@link Flippable#disabledValue() } is * specified in the annotation of the method. */ public void setDefaultValue(String defaultValue) { this.defaultValue = defaultValue; } /** * A pointcut declaration which marks all methods in the class. */ @Pointcut("execution(* *(..))") public void anyMethod() { } /** * Intercept calls to the methods marked with {@link Flippable} annotation. * Firstly it processes the method parameters marked with {@link FlipParam} * annotation. Each parameter value is replaced with the value declared in * {@link FlipParam#disabledValue()} if the feature declared in {@link FlipParam#feature() } * is disabled. * After properties evaluation this method checks if the feature marked in * {@link Flippable#feature() } is disabled then the value from {@link Flippable#disabledValue() } * is returned. * * @param flip the {@link Flippable} annotation instance which marks the method to intercept. * @param pjp the object obtained from AspectJ method interceptor. * @return the processed value of the method depending from feature state. * @throws Throwable */ @Around(value = "anyMethod() && @annotation(flip)", argNames = "flip") public Object aroundFlippableMethods(ProceedingJoinPoint pjp, Flippable flip) throws Throwable { MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); Method method = methodSignature.getMethod(); if (isFeatureEnabled(flip.feature())) { Annotation[][] paramAnnotations = method.getParameterAnnotations(); Object[] params = pjp.getArgs(); Class[] paramTypes = method.getParameterTypes(); for (int i = 0; i < paramAnnotations.length; i++) { FlipParam flipParam = findFlipParamAnnoattion(paramAnnotations[i]); if (!isFeatureEnabled(flipParam.feature())) { params[i] = getProcessedDisabledValue(paramTypes[i], flipParam.disabledValue(), pjp.getThis()); } } return pjp.proceed(params); } else { return getProcessedDisabledValue(method.getReturnType(), flip.disabledValue(), pjp.getThis()); } } public Object aroundFlippableMethods(ProceedingJoinPoint pjp) throws Throwable { final Flippable flip = ((MethodSignature) pjp.getSignature()).getMethod().getAnnotation(Flippable.class); return aroundFlippableMethods(pjp, flip); } /** * Returns if the feature is enabled. If the feature is empty then the feature * is considered enabled. * * @param feature the name of the feature to test. * @return true if the feature is enabled or the feature name is empty. */ private boolean isFeatureEnabled(String feature) { return feature == null || feature.isEmpty() || getFeatureService().getFeatureState(feature) == FeatureState.ENABLED; } /** * Finds the {@link FlipParam} annotation instance from a list of annotations. * * @param annotations the list of annotation which marks the parameter. * @return the {@link FlipParam} instance if found, otherwise null. */ private FlipParam findFlipParamAnnoattion(Annotation[] annotations) { for (Annotation annotation : annotations) { if (FlipParam.class.isAssignableFrom(annotation.annotationType())) { return (FlipParam) annotation; } } return null; } /** * Processes the disabled value used. If the value contains the <code>${}</code> * statement then this statement is processed using {@link #getValueExpressionEvaluator() }, * otherwise it is used converter found by {@link #getConvertersHandler() }. * The converter is found by ouputClass parameter. If no converter is found and * the outputClass is not a {@link CharSequence} then {@link IllegalArgumentException} * is thrown. * * @param outputClass the required object type * @param value the disabled value to evaluate. * @param context the object of the method intercepted. This object is used as context to * evaluate value expression. * * @return the evaluated value * @throws IllegalArgumentException if cannot find the converter. */ private Object getProcessedDisabledValue(Class outputClass, String value, Object context) { if (value == null || value.isEmpty()) { value = defaultValue; } Matcher m = VALUE_EXPRESSION_REGEX.matcher(value); if (m.find()) { ValueExpressionEvaluator evaluator = getValueExpressionEvaluator(); if (evaluator == null) { return getUntouchedDisabledValue(outputClass, value); } return evaluator.evaluate(context, m.group(1)); } else { Converter converter = getConvertersHandler().getConverter(outputClass); if (converter == null) { return getUntouchedDisabledValue(outputClass, value); } return converter.convert(value, outputClass); } } /** * Returns the value if it cannot be converted or evaluated as value expression. * If the outputClass parameter is no {@link CharSequence} then the * {@link IllegalArgumentException} is thrown, otherwise the value itself is * returned. * * @param outputClass the required output object type. * @param value the value. * @return the value string if is possible. * @throws IllegalArgumentException if cannot convert value parameter to outputClass. */ private Object getUntouchedDisabledValue(Class outputClass, String value) { if (CharSequence.class.isAssignableFrom(outputClass)) { return value; } else { throw new IllegalArgumentException( String.format("Cannot find converter for class [%s].", outputClass.getName())); } } }