package lombok.eclipse; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; import java.util.ServiceLoader; import lombok.eclipse.EclipseAST.Node; import lombok.transformations.TypeLibrary; import lombok.transformations.TypeResolver; import org.eclipse.jdt.core.compiler.CategorizedProblem; import org.eclipse.jdt.internal.compiler.CompilationResult; import org.eclipse.jdt.internal.compiler.apt.dispatch.AptProblem; import org.eclipse.jdt.internal.compiler.ast.ArrayInitializer; import org.eclipse.jdt.internal.compiler.ast.ClassLiteralAccess; import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; import org.eclipse.jdt.internal.compiler.ast.Expression; import org.eclipse.jdt.internal.compiler.ast.ImportReference; import org.eclipse.jdt.internal.compiler.ast.Literal; import org.eclipse.jdt.internal.compiler.ast.MemberValuePair; import org.eclipse.jdt.internal.compiler.ast.NameReference; import org.eclipse.jdt.internal.compiler.ast.QualifiedNameReference; import org.eclipse.jdt.internal.compiler.ast.SingleNameReference; import org.eclipse.jdt.internal.compiler.ast.TypeReference; import org.eclipse.jdt.internal.compiler.impl.Constant; import org.eclipse.jdt.internal.compiler.impl.ReferenceContext; import org.eclipse.jdt.internal.compiler.lookup.TypeIds; import org.eclipse.jdt.internal.compiler.problem.ProblemSeverities; import org.eclipse.jdt.internal.compiler.util.Util; public class HandlerLibrary { private TypeLibrary typeLibrary = new TypeLibrary(); private static class HandlerContainer { private EclipseAnnotationHandler handler; private Class annotationClass; HandlerContainer(EclipseAnnotationHandler handler, Class annotationClass) { this.handler = handler; this.annotationClass = annotationClass; } @SuppressWarnings("unchecked") public void handle(Object annInstance, org.eclipse.jdt.internal.compiler.ast.Annotation annotation, Node node) { handler.handle((T) annInstance, annotation, node); } } private Map> handlers = new HashMap>(); @SuppressWarnings("unchecked") public A createAnnotation(Class target, CompilationUnitDeclaration ast, org.eclipse.jdt.internal.compiler.ast.Annotation node) throws EnumDecodeFail { final Map values = new HashMap(); final MemberValuePair[] pairs = node.memberValuePairs(); for ( Method m : target.getMethods() ) { String name = m.getName(); Object value = m.getDefaultValue(); for ( MemberValuePair pair : pairs ) { if ( name.equals(new String(pair.name)) ) { value = calculateValue(pair, ast, m.getReturnType(), pair.value); break; } } values.put(name, value); } InvocationHandler invocations = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return values.get(method.getName()); } }; return (A) Proxy.newProxyInstance(target.getClassLoader(), new Class[] { target }, invocations); } private Object calculateValue(MemberValuePair pair, CompilationUnitDeclaration ast, Class type, Expression e) throws EnumDecodeFail { if ( e instanceof Literal ) { ((Literal)e).computeConstant(); return convertConstant(pair, type, e.constant); } else if ( e instanceof ArrayInitializer ) { if ( !type.isArray() ) throw new EnumDecodeFail(pair, "Did not expect an array here."); Class component = type.getComponentType(); Expression[] expressions = ((ArrayInitializer)e).expressions; int length = expressions == null ? 0 : expressions.length; Object[] values = new Object[length]; for (int i = 0; i < length; i++) { values[i] = calculateValue(pair, ast, component, expressions[i]); } return values; } else if ( e instanceof ClassLiteralAccess ) { if ( type == Class.class ) return toClass(pair, ast, str(((ClassLiteralAccess)e).type.getTypeName())); else throw new EnumDecodeFail(pair, "Expected a " + type + " literal."); } else if ( e instanceof NameReference ) { String s = null; if ( e instanceof SingleNameReference ) s = new String(((SingleNameReference)e).token); else if ( e instanceof QualifiedNameReference ) s = str(((QualifiedNameReference)e).tokens); if ( Enum.class.isAssignableFrom(type) ) return toEnum(pair, type, s); throw new EnumDecodeFail(pair, "Lombok annotations must contain literals only."); } else { throw new EnumDecodeFail(pair, "Lombok could not decode this annotation parameter."); } } private Enum toEnum(MemberValuePair pair, Class enumType, String ref) throws EnumDecodeFail { int idx = ref.indexOf('.'); if ( idx > -1 ) ref = ref.substring(idx +1); Object[] enumConstants = enumType.getEnumConstants(); for ( Object constant : enumConstants ) { String target = ((Enum)constant).name(); if ( target.equals(ref) ) return (Enum) constant; } throw new EnumDecodeFail(pair, "I can't figure out which enum constant you mean."); } private Class toClass(MemberValuePair pair, CompilationUnitDeclaration ast, String typeName) throws EnumDecodeFail { Class c; boolean fqn = typeName.indexOf('.') > -1; if ( fqn ) { c = tryClass(typeName); if ( c != null ) return c; } for ( ImportReference ref : ast.imports ) { String im = str(ref.tokens); int idx = im.lastIndexOf('.'); String simple = im; if ( idx > -1 ) simple = im.substring(idx+1); if ( simple.equals(typeName) ) { c = tryClass(im); if ( c != null ) return c; } } if ( ast.currentPackage != null && ast.currentPackage.tokens != null ) { String pkg = str(ast.currentPackage.tokens); c = tryClass(pkg + "." + typeName); if ( c != null ) return c; } c = tryClass("java.lang." + typeName); if ( c != null ) return c; throw new EnumDecodeFail(pair, "I can't find this class. Try using the fully qualified name."); } private Class tryClass(String name) { try { return Class.forName(name); } catch ( ClassNotFoundException e ) { return null; } } private Object convertConstant(MemberValuePair pair, Class type, Constant constant) throws EnumDecodeFail { int targetTypeID; boolean array = type.isArray(); if ( array ) type = type.getComponentType(); if ( type == int.class ) targetTypeID = TypeIds.T_int; else if ( type == long.class ) targetTypeID = TypeIds.T_long; else if ( type == short.class ) targetTypeID = TypeIds.T_short; else if ( type == byte.class ) targetTypeID = TypeIds.T_byte; else if ( type == double.class ) targetTypeID = TypeIds.T_double; else if ( type == float.class ) targetTypeID = TypeIds.T_float; else if ( type == String.class ) targetTypeID = TypeIds.T_JavaLangString; else if ( type == char.class ) targetTypeID = TypeIds.T_char; else if ( type == boolean.class ) targetTypeID = TypeIds.T_boolean; else { //Enum or Class, so a constant isn't going to be very useful. throw new EnumDecodeFail(pair, "Expected a constant of some sort here (a number or a string)"); } if ( !Expression.isConstantValueRepresentable(constant, constant.typeID(), targetTypeID) ) { throw new EnumDecodeFail(pair, "I can't turn this literal into a " + type); } Object o = null; if ( type == int.class ) o = constant.intValue(); else if ( type == long.class ) o = constant.longValue(); else if ( type == short.class ) o = constant.shortValue(); else if ( type == byte.class ) o = constant.byteValue(); else if ( type == double.class ) o = constant.doubleValue(); else if ( type == float.class ) o = constant.floatValue(); else if ( type == String.class ) o = constant.stringValue(); else if ( type == char.class ) o = constant.charValue(); else if ( type == boolean.class ) o = constant.booleanValue(); if ( array ) { Object a = Array.newInstance(type, 1); Array.set(a, 0, o); return a; } return o; } private static class EnumDecodeFail extends Exception { private static final long serialVersionUID = 1L; MemberValuePair pair; EnumDecodeFail(MemberValuePair pair, String msg) { super(msg); this.pair = pair; } } private static String str(char[][] c) { boolean first = true; StringBuilder sb = new StringBuilder(); for ( char[] part : c ) { sb.append(first ? "" : ".").append(part); first = false; } return sb.toString(); } @SuppressWarnings("unchecked") public static HandlerLibrary load() { HandlerLibrary lib = new HandlerLibrary(); for ( EclipseAnnotationHandler handler : ServiceLoader.load(EclipseAnnotationHandler.class) ) { Class annotationClass = lib.findAnnotationClass(handler.getClass()); HandlerContainer container = new HandlerContainer(handler, annotationClass); lib.handlers.put(container.annotationClass.getName(), container); lib.typeLibrary.addType(container.annotationClass.getName()); } return lib; } @SuppressWarnings("unchecked") private Class findAnnotationClass(Class c) { if ( c == Object.class || c == null ) return null; for ( Type iface : c.getGenericInterfaces() ) { if ( iface instanceof ParameterizedType ) { ParameterizedType p = (ParameterizedType)iface; if ( !EclipseAnnotationHandler.class.equals(p.getRawType()) ) continue; Type target = p.getActualTypeArguments()[0]; if ( target instanceof Class ) { if ( Annotation.class.isAssignableFrom((Class) target) ) { return (Class) target; } } throw new ClassCastException("Not an annotation type: " + target); } } Class potential = findAnnotationClass(c.getSuperclass()); if ( potential != null ) return potential; for ( Class iface : c.getInterfaces() ) { potential = findAnnotationClass(iface); if ( potential != null ) return potential; } return null; } public void handle(CompilationUnitDeclaration ast, EclipseAST.Node node, org.eclipse.jdt.internal.compiler.ast.Annotation annotation) { TypeResolver resolver = new TypeResolver(typeLibrary, node.top()); TypeReference rawType = annotation.type; if ( rawType == null ) return; for ( String fqn : resolver.findTypeMatches(node, annotation.type) ) { HandlerContainer container = handlers.get(fqn); if ( container == null ) continue; try { Object annInstance = createAnnotation(container.annotationClass, ast, annotation); container.handle(annInstance, annotation, node); } catch (EnumDecodeFail e) { if ( ast.compilationResult != null ) { Node referenceContextNode = node; while ( !(referenceContextNode.getEclipseNode() instanceof ReferenceContext) ) { referenceContextNode = referenceContextNode.up(); } ReferenceContext referenceContext = (ReferenceContext)referenceContextNode.getEclipseNode(); char[] fileName = node.getFileName().toCharArray(); String message = e.getMessage(); int lineNumber = 0; int columnNumber = 1; int startPosition = e.pair.sourceStart; int endPosition = e.pair.sourceEnd; if (referenceContext != null) { CompilationResult result = referenceContext.compilationResult(); int[] lineEnds = null; lineNumber = startPosition >= 0 ? Util.getLineNumber(startPosition, lineEnds = result.getLineSeparatorPositions(), 0, lineEnds.length-1) : 0; columnNumber = startPosition >= 0 ? Util.searchColumnNumber(result.getLineSeparatorPositions(), lineNumber,startPosition) : 0; } CategorizedProblem problem = new AptProblem(referenceContext, fileName, message, 0, new String[0], ProblemSeverities.Error, startPosition, endPosition, lineNumber, columnNumber); ast.compilationResult.record(problem, referenceContext); } } } } }