blob: e0d8bacb88575ddd9d28dccc9f82834b4d1a132c [file] [log] [blame]
/*
* Copyright 2003-2007 the original author or 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 org.codehaus.groovy.classgen;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.expr.AnnotationConstantExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.control.ErrorCollector;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.syntax.SyntaxException;
/**
* An Annotation visitor responsible with:
* - reading annotation metadata (@Retention, @Target, attribute types)
* - verify that an <code>AnnotationNode</code> conforms to annotation meta
* - enhancing an <code>AnnotationNode</code> AST to reflect real annotation meta
*
* @author <a href='mailto:the[dot]mindstorm[at]gmail[dot]com'>Alex Popescu</a>
*/
public class AnnotationVisitor {
private static final Class[] EMPTY_ARG_TYPES = new Class[0];
private static final Object[] EMPTY_ARGS = new Object[0];
private final Class annotationRootClass;
private SourceUnit source;
private ErrorCollector errorCollector;
private AnnotationNode annotation;
private Class annotationClass;
private Map requiredAttrTypes = new HashMap(); // Map<String, Class>
private Map defaultAttrTypes = new HashMap(); // Map<String, Class>
public AnnotationVisitor(SourceUnit source, ErrorCollector errorCollector) {
this.source = source;
this.errorCollector = errorCollector;
this.annotationRootClass = loadAnnotationRootClass();
}
public AnnotationNode visit(AnnotationNode node) {
if(!isValidAnnotationClass(node)) {
node.setValid(false);
return node;
}
this.annotation = node;
if(!node.getClassNode().isResolved()) {
addError("Current type was not yet resolved. Cannot introspect it.");
node.setValid(false);
return node;
}
this.annotationClass = node.getClassNode().getTypeClass();
extractAnnotationMeta(this.annotationClass);
if(this.errorCollector.hasErrors()) {
this.annotation.setValid(false);
return this.annotation;
}
Map attributes = this.annotation.getMembers();
for(Iterator it = attributes.entrySet().iterator(); it.hasNext(); ) {
Map.Entry entry = (Map.Entry) it.next();
String attrName = (String) entry.getKey();
Expression attrExpr = (Expression) entry.getValue();
Class attrType = getAttributeType(attrName);
if(attrType == null) {
addError("Unknown attribute '" + attrName + "'", attrExpr);
break;
}
visitExpression(attrName, attrExpr, attrType);
}
if(!this.requiredAttrTypes.isEmpty()) {
addError("Required attributes " + this.requiredAttrTypes.keySet() + " not found",
this.annotation);
}
this.annotation.setValid(!this.errorCollector.hasErrors());
return this.annotation;
}
/**
* @param node
* @return
*/
private boolean isValidAnnotationClass(AnnotationNode node) {
return node.getClassNode().implementsInterface("java.lang.annotation.Annotation");
}
protected void visitExpression(String attrName, Expression attrAst, Class attrType) {
if(attrType.isArray()) {
// check needed as @Test(attr = {"elem"}) passes through the parser
if(attrAst instanceof ListExpression) {
visitListExpression(attrName, (ListExpression) attrAst, attrType.getComponentType());
}
else {
addError("Annotation list attributes must use Groovy notation [el1, el2]", attrAst);
}
}
if(attrType.isPrimitive()) {
visitConstantExpression(attrName, (ConstantExpression) attrAst, ClassHelper.getWrapper(ClassHelper.make(attrType)));
}
else if(String.class.equals(attrType)) {
visitConstantExpression(attrName, (ConstantExpression) attrAst, ClassHelper.make(String.class));
}
else if(Class.class.equals(attrType)) {
// there is nothing to check about ClassExpressions
}
else if(isEnum(attrType)) {
visitEnumExpression(attrName, (PropertyExpression) attrAst, ClassHelper.make(attrType));
}
else if(isAnnotation(attrType)) {
visitAnnotationExpression(attrName, (AnnotationConstantExpression) attrAst, attrType);
}
}
/**
* @param attrName
* @param expression
* @param attrType
*/
protected void visitAnnotationExpression(String attrName, AnnotationConstantExpression expression, Class attrType) {
AnnotationNode annotationNode = (AnnotationNode) expression.getValue();
AnnotationVisitor visitor = new AnnotationVisitor(this.source, this.errorCollector);
visitor.visit(annotationNode);
}
protected void visitListExpression(String attrName, ListExpression listExpr, Class elementType) {
List expressions = listExpr.getExpressions();
for (int i = 0; i < expressions.size(); i++) {
visitExpression(attrName, (Expression) expressions.get(i), elementType);
}
}
protected void visitConstantExpression(String attrName, ConstantExpression constExpr, ClassNode attrType) {
if(!constExpr.getType().isDerivedFrom(attrType)) {
addError("Attribute '" + attrName + "' should have type '" + attrType.getName() + "'; "
+ "but found type '" + constExpr.getType().getName() + "'",
constExpr);
}
}
protected void visitEnumExpression(String attrName, PropertyExpression propExpr, ClassNode attrType) {
if(!propExpr.getObjectExpression().getType().isDerivedFrom(attrType)) {
addError("Attribute '" + attrName + "' should have type '" + attrType.getName() +"' (Enum), but found "
+ propExpr.getObjectExpression().getType().getName(),
propExpr);
}
}
private boolean isAnnotation(Class clazz) {
Boolean result = (Boolean) invoke(clazz.getClass(), "isAnnotation", EMPTY_ARG_TYPES, clazz, EMPTY_ARGS);
return result.booleanValue();
}
private boolean isEnum(Class clazz) {
Boolean result = (Boolean) invoke(clazz.getClass(), "isEnum", EMPTY_ARG_TYPES, clazz, EMPTY_ARGS);
return result.booleanValue();
}
private void extractAnnotationMeta(Class annotationClass) {
initializeAnnotationMeta(annotationClass);
initializeAttributeTypes(annotationClass);
}
private void initializeAnnotationMeta(Class annotationClass) {
Object[] annotations = (Object[]) invoke(annotationClass.getClass(),
"getAnnotations", EMPTY_ARG_TYPES, annotationClass, EMPTY_ARGS);
if (annotations == null) {
addError("Cannot retrieve annotation meta information. "
+ ExtendedVerifier.JVM_ERROR_MESSAGE);
return;
}
for(int i = 0; i < annotations.length; i++) {
Class annotationType = (Class) invoke(this.annotationRootClass,
"annotationType", EMPTY_ARG_TYPES, annotations[i], EMPTY_ARGS);
if (annotationType == null) continue;
if ("java.lang.annotation.Retention".equals(annotationType.getName())) {
initializeRetention(annotationClass, annotationType, annotations[i]);
}
else if("java.lang.annotation.Target".equals(annotationType.getName())) {
initializeTarget(annotationClass, annotationType, annotations[i]);
}
}
}
private void initializeAttributeTypes(Class annotationClass) {
Method[] methods = annotationClass.getDeclaredMethods();
for(int i = 0; i < methods.length; i++) {
Object defaultValue = invoke(Method.class, "getDefaultValue", EMPTY_ARG_TYPES, methods[i], EMPTY_ARGS);
if (defaultValue != null) {
// by now we know JDK1.5 API is available so a null means no default value
defaultAttrTypes.put(methods[i].getName(), methods[i].getReturnType());
}
else {
requiredAttrTypes.put(methods[i].getName(), methods[i].getReturnType());
}
}
}
private void initializeRetention(Class annotationClass, Class retentionClass, Object retentionAnnotation) {
Object retentionPolicyEnum =
invoke(retentionClass, "value", EMPTY_ARG_TYPES, retentionAnnotation, EMPTY_ARGS);
if (retentionPolicyEnum == null) {
addError("Cannot read @RetentionPolicy on the @" + annotationClass.getName()
+ ExtendedVerifier.JVM_ERROR_MESSAGE);
return;
}
if("RUNTIME".equals(retentionPolicyEnum.toString())) {
this.annotation.setRuntimeRetention(true);
}
else if("SOURCE".equals(retentionPolicyEnum.toString())) {
this.annotation.setSourceRetention(true);
}
}
private void initializeTarget(Class annotationClass, Class targetClass, Object targetAnnotation) {
Object[] elementTypeEnum =
(Object[]) invoke(targetClass, "value", EMPTY_ARG_TYPES, targetAnnotation, EMPTY_ARGS);
if (elementTypeEnum == null) {
addError("Cannot read @Target on the @" + annotationClass.getName()
+ ExtendedVerifier.JVM_ERROR_MESSAGE);
return;
}
int bitmap = 0;
for (int i = 0; i < elementTypeEnum.length; i++) {
String targetName = elementTypeEnum[i].toString();
if("TYPE".equals(targetName)) {
bitmap |= AnnotationNode.TYPE_TARGET;
}
else if("CONSTRUCTOR".equals(targetName)) {
bitmap |= AnnotationNode.CONSTRUCTOR_TARGET;
}
else if("METHOD".equals(targetName)) {
bitmap |= AnnotationNode.METHOD_TARGET;
}
else if("FIELD".equals(targetName)) {
bitmap |= AnnotationNode.FIELD_TARGET;
}
else if("PARAMETER".equals(targetName)) {
bitmap |= AnnotationNode.PARAMETER_TARGET;
}
else if("LOCAL_VARIABLE".equals(targetName)) {
bitmap |= AnnotationNode.LOCAL_VARIABLE_TARGET;
}
else if("ANNOTATION".equals(targetName)) {
bitmap |= AnnotationNode.ANNOTATION_TARGET;
}
}
this.annotation.setAllowedTargets(bitmap);
}
protected void addError(String msg) {
this.errorCollector.addErrorAndContinue(
new SyntaxErrorMessage(new SyntaxException(msg
+ " in @" + this.annotationClass.getName() + '\n',
this.annotation.getLineNumber(),
this.annotation.getColumnNumber()), this.source)
);
}
protected void addError(String msg, ASTNode expr) {
this.errorCollector.addErrorAndContinue(
new SyntaxErrorMessage(new SyntaxException(msg
+ " in @" + this.annotationClass.getName() + '\n',
expr.getLineNumber(),
expr.getColumnNumber()), this.source)
);
}
private Class getAttributeType(String attr) {
if(this.requiredAttrTypes.containsKey(attr)) {
return (Class) this.requiredAttrTypes.remove(attr);
}
return (Class) this.defaultAttrTypes.remove(attr);
}
private Object invoke(Class clazz, String methodName, Class[] argTypes, Object target, Object[] args) {
try {
Method m = clazz.getMethod(methodName, argTypes);
return m.invoke(target, args);
}
catch(Throwable cause) {
// we report an error on called side
}
return null;
}
private Class loadAnnotationRootClass() {
try {
return Class.forName("java.lang.annotation.Annotation");
}
catch(Throwable cause) {
// report the error later
}
return null;
}
}