/*********************************************************************
Copyright 2014 the Flapi 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 unquietcode.tools.flapi.annotations;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import unquietcode.tools.flapi.Constants;
import unquietcode.tools.flapi.DescriptorBuilderException;
import unquietcode.tools.flapi.IntrospectorSupport;
import unquietcode.tools.flapi.beans.BeanIntrospector;
import unquietcode.tools.flapi.builder.Annotation.AnnotationHelper;
import unquietcode.tools.flapi.builder.Block.BlockHelper;
import unquietcode.tools.flapi.builder.Documentation.DocumentationHelper;
import unquietcode.tools.flapi.builder.Method.MethodHelper;
import unquietcode.tools.flapi.helpers.BlockHelperImpl;
import unquietcode.tools.flapi.helpers.MethodHelperImpl;
import unquietcode.tools.flapi.outline.BlockOutline;
import unquietcode.tools.flapi.outline.DescriptorOutline;
import unquietcode.tools.flapi.outline.MethodOutline;
import unquietcode.tools.flapi.runtime.EnumSelectorHint;
import unquietcode.tools.flapi.runtime.Helpers;
import unquietcode.tools.flapi.runtime.SpringMethodUtils;
import unquietcode.tools.spring.generics.MethodParameter;
import unquietcode.tools.spring.generics.ResolvableType;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
/**
* @author Ben Fagin
* @version 2014-08-03
*/
public class AnnotationIntrospector extends IntrospectorSupport {
private final Map<Class<?>, BlockOutline> blocks = new HashMap<Class<?>, BlockOutline>();
public static boolean isAnnotated(Class<?> clazz) {
// for every method in the class
for (Method method : getAllMethods(clazz)) {
List<Annotation> methodQuantifiers = findAnnotatedElements(MethodQuantifier.class, method.getAnnotations());
// if the method has at least one marker
// then it is annotated (even though having
// more than one marker is technically an
// error, the intent is still clear)
if (methodQuantifiers.size() > 0) {
return true;
}
}
// we didn't find any methods
return false;
}
private static Set<Method> getAllMethods(Class<?> clazz) {
Method[] methods = SpringMethodUtils.getUniqueDeclaredMethods(clazz);
return new HashSet<>(Arrays.asList(methods));
}
public static DescriptorOutline createDescriptor(Class<?> clazz) {
DescriptorOutline descriptor = new DescriptorOutline();
descriptor.setPackageName(clazz.getPackage().getName() + ".builder");
// discover methods and set them on the blocks
AnnotationIntrospector introspector = new AnnotationIntrospector();
// class
boolean found = introspector.handleClass(descriptor, clazz);
// interfaces (interface extensions are handled automatically by Spring)
if (!clazz.isInterface()) {
for (Class<?> intf : clazz.getInterfaces()) {
found |= introspector.handleClass(descriptor, intf);
}
}
if (found) {
return descriptor;
}
// if we didn't find any, try the bean introspector
BeanIntrospector beanIntrospector = new BeanIntrospector();
descriptor = beanIntrospector.createDescriptor(clazz);
return descriptor;
}
public static BlockOutline createBlock(Class<?> clazz) {
return createDescriptor(clazz);
}
private BlockOutline handleClass(Class<?> blockClass) {
if (blocks.containsKey(blockClass)) {
return blocks.get(blockClass);
}
BlockOutline outline = new BlockOutline();
blocks.put(blockClass, outline);
handleClass(outline, blockClass);
return outline;
}
private boolean handleClass(BlockOutline blockOutline, Class<?> blockClass) {
BlockHelper helper = new BlockHelperImpl(blockOutline);
Block block = blockClass.getAnnotation(Block.class);
blockOutline.setHelperClass(blockClass);
// block name
if (block != null && !block.name().trim().isEmpty()) {
blockOutline.setName(block.name());
} else {
blockOutline.setName(blockClass.getSimpleName());
}
boolean atLeastOne = false;
for (Method method : getAllMethods(blockClass)) {
List<Annotation> methodQuantifiers = findAnnotatedElements(MethodQuantifier.class, method.getAnnotations());
// skip methods which are not annotated
if (methodQuantifiers.size() == 0) {
continue;
}
// only allow one quantifier annotation per method
if (methodQuantifiers.size() > 1) {
throw new DescriptorBuilderException(
"Only one annotation from " +
"{@Any, @AtLeast, @AtMost, @Between, @Exactly, @Last}" +
" is allowed per method."
);
}
handleMethod(helper, method);
atLeastOne = true;
}
return atLeastOne;
}
private void handleMethod(final BlockHelper blockHelper, final Method method) {
final After after = method.getAnnotation(After.class);
final Any any = method.getAnnotation(Any.class);
final AtLeast atLeast = method.getAnnotation(AtLeast.class);
final AtMost atMost = method.getAnnotation(AtMost.class);
final Between between = method.getAnnotation(Between.class);
final Documented documented = method.getAnnotation(Documented.class);
final Exactly exactly = method.getAnnotation(Exactly.class);
final Last last = method.getAnnotation(Last.class);
final EnumSelector selector = method.getAnnotation(EnumSelector.class);
final String methodSignature = getMethodSignature(method);
final MethodHelper methodHelper;
// is it an enum selector?
if (selector != null) {
final Class<?>[] parameterTypes = method.getParameterTypes();
// check parameter length
if (parameterTypes.length != 0) {
throw new DescriptorBuilderException("@EnumSelector methods must have zero arguments");
}
// check return type
if (!Consumer.class.isAssignableFrom(method.getReturnType())) {
throw new DescriptorBuilderException("@EnumSelector methods must return Consumer<EnumType>");
}
final Class<?> genericEnum = ResolvableType.forMethodReturnType(method).resolveGeneric(0);
// check consumer enum type
if (!genericEnum.isEnum()) {
throw new DescriptorBuilderException("@EnumSelector methods must return a consumer of Enum types");
}
methodHelper = Helpers.invoke((_helper) -> {
blockHelper.addEnumSelector(genericEnum, methodSignature, _helper);
});
AnnotationHelper annotationHelper = Helpers.invoke((_helper) -> {
methodHelper.addAnnotation(EnumSelectorHint.class, _helper);
});
annotationHelper.withParameter("value", genericEnum);
annotationHelper.finish();
}
// regular method
else {
methodHelper = Helpers.invoke((_helper) -> {
blockHelper.addMethod(methodSignature, _helper);
});
}
// TODO meh
final MethodOutline methodOutline = ((MethodHelperImpl) methodHelper).getOutline();
// @After
if (after != null) {
methodHelper.after(after.value());
}
// @Any
if (any != null) {
if (any.group() == Constants.DEFAULT_NULL_INT) {
methodHelper.any();
} else {
methodHelper.any(any.group());
}
}
// @AtLeast
if (atLeast != null) {
methodHelper.atLeast(atLeast.value());
}
// @AtMost
if (atMost != null) {
if (atMost.group() == Constants.DEFAULT_NULL_INT) {
methodHelper.atMost(atMost.value());
} else {
methodHelper.atMost(atMost.value(), atMost.group());
}
}
// @Between
if (between != null) {
methodHelper.between(between.minInc(), between.maxInc());
}
// @Exactly
if (exactly != null) {
methodHelper.exactly(exactly.value());
}
// @Last
if (last != null) {
Class<?> returnType = method.getReturnType();
if (returnType == void.class) {
methodHelper.last();
} else {
Class<?>[] generics = ResolvableType.forMethodReturnType(method).resolveGenerics();
methodHelper.last(makeTypeWithGenerics(returnType, generics));
}
}
// ensure that non-terminal methods aren't returning anything
else if (selector == null) {
Class<?> returnType = method.getReturnType();
if (returnType != void.class) {
throw new DescriptorBuilderException(
"Only @Last methods can return anything; all other methods must be 'void'.\n\t" +
"(for method '"+method.getDeclaringClass().getName()+"#"+method.getName()+"')\n"
);
}
}
// @Documented
if (documented != null) {
DocumentationHelper docHelper = Helpers.invoke((_helper) -> {
methodHelper.withDocumentation(_helper);
});
for (String docString : documented.value()) {
docHelper.addContent(docString);
}
docHelper.finish();
}
// other annotations
for (Annotation annotation : method.getAnnotations()) {
// skip over Flapi's own annotation types
if (annotation.annotationType().getAnnotation(FlapiAnnotation.class) != null) {
continue;
}
handleMethodAnnotation(methodHelper, annotation);
}
// block chaining
final Parameter[] parameters = method.getParameters();
for (int i=0; i < parameters.length; ++i) {
final Parameter parameter = parameters[i];
final BlockChain blockChain = getParameterAnnotation(parameter, BlockChain.class);
if (blockChain != null) {
// check type
if (parameter.getType() != AtomicReference.class) {
throw new DescriptorBuilderException("@BlockChain parameters must be of type AtomicReference");
}
// get the generic type of the reference
Class<?> generic = ResolvableType.forMethodParameter(MethodParameter.forMethodOrConstructor(method, i)).resolveGeneric();
// handle the reference type
BlockOutline blockOutline = handleClass(generic);
methodOutline.getBlockChain().add(blockOutline);
methodOutline.getChainParameterPositions().add(i);
}
}
}
private static void handleMethodAnnotation(final MethodHelper methodHelper, Annotation annotation) {
final Class<? extends Annotation> annotationClass = annotation.annotationType();
AnnotationHelper annotationHelper = Helpers.invoke((_helper) -> {
methodHelper.addAnnotation(annotationClass, _helper);
});
for (Method method : annotationClass.getDeclaredMethods()) {
annotationHelper.withParameter(method.getName(), method.getReturnType());
}
annotationHelper.finish();
}
private static <
_MarkerAnnotation extends Annotation
>
List<Annotation> findAnnotatedElements(
Class<_MarkerAnnotation> marker, Annotation...elements
){
return findAnnotatedElements(marker, elements, new Function<Annotation, Class<? extends Annotation>>() {
public Class<? extends Annotation> apply(Annotation input) {
return input.annotationType();
}
});
}
private static <
_MarkerAnnotation extends Annotation,
_AnnotatedElement extends AnnotatedElement
>
List<_AnnotatedElement> findAnnotatedElements(
Class<_MarkerAnnotation> marker, _AnnotatedElement...elements
){
return findAnnotatedElements(marker, elements, Functions.<_AnnotatedElement>identity());
}
private static <
_MarkerAnnotation extends Annotation,
_AnnotatedElement extends AnnotatedElement,
_BaseElement
>
List<_BaseElement> findAnnotatedElements(
Class<_MarkerAnnotation> marker, _BaseElement[] elements, Function<_BaseElement, _AnnotatedElement> function
){
List<_BaseElement> annotated = new ArrayList<_BaseElement>();
for (_BaseElement element : elements) {
_AnnotatedElement annotatedElement = function.apply(element);
_MarkerAnnotation annotation = annotatedElement.getAnnotation(marker);
if (annotation != null) {
annotated.add(element);
}
}
return annotated;
}
}