blob: 2c5e090f307e42f95f3e84981c151da158d8fa7e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.transform;
import groovy.lang.GroovyRuntimeException;
import groovy.transform.AnnotationCollector;
import org.apache.groovy.ast.tools.ClassNodeUtils;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.InnerClassNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.AnnotationConstantExpression;
import org.codehaus.groovy.ast.expr.ArrayExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
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.MapExpression;
import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.SourceUnit;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.propX;
import static org.codehaus.groovy.transform.trait.TraitComposer.COMPILESTATIC_CLASSNODE;
import static org.objectweb.asm.Opcodes.ACC_ABSTRACT;
import static org.objectweb.asm.Opcodes.ACC_ANNOTATION;
import static org.objectweb.asm.Opcodes.ACC_ENUM;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_INTERFACE;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
/**
* This class is the base for any annotation alias processor.
*
* @see AnnotationCollector
* @see AnnotationCollectorTransform#visit(AnnotationNode, AnnotationNode, AnnotatedNode, SourceUnit)
*/
public class AnnotationCollectorTransform {
private static List<AnnotationNode> getMeta(ClassNode cn) {
List<AnnotationNode> meta = cn.getNodeMetaData(AnnotationCollector.class);
if (meta == null) {
if (cn.isPrimaryClassNode()) {
meta = getTargetListFromAnnotations(cn);
} else {
meta = getTargetListFromClass(cn);
}
cn.setNodeMetaData(AnnotationCollector.class, meta);
}
return meta;
}
/**
* Class used by {@link org.codehaus.groovy.control.CompilationUnit} to transform the alias class
* into what is needed by the compiler. This means removing invalid
* modifiers, interfaces and superclasses, as well as adding a static
* value method returning our serialized version of the data for processing
* from a pre-compiled state. By doing this the old annotations will be
* removed as well
*/
public static class ClassChanger {
/**
* Method to transform the given ClassNode, if it is annotated with
* {@link AnnotationCollector}. See class description for what the
* transformation includes.
*/
public void transformClass(ClassNode cn) {
AnnotationNode collector = null;
for (AnnotationNode an : cn.getAnnotations()) {
if (an.getClassNode().getName().equals(AnnotationCollector.class.getName())) {
collector = an;
break;
}
}
if (collector == null) {
return;
}
boolean legacySerialization = false;
final Expression member = collector.getMember("serializeClass");
if (member instanceof ClassExpression) {
ClassExpression ce = (ClassExpression) member;
legacySerialization = ce.getType().getName().equals(cn.getName());
}
ClassNode helper = cn;
if (legacySerialization) {
// force final class, remove interface, annotation, enum and abstract modifiers
helper.setModifiers((ACC_FINAL | helper.getModifiers()) & ~(ACC_ENUM | ACC_INTERFACE | ACC_ANNOTATION | ACC_ABSTRACT));
// force Object super class
helper.setSuperClass(ClassHelper.OBJECT_TYPE);
// force no interfaces implemented
helper.setInterfaces(ClassNode.EMPTY_ARRAY);
} else {
helper = new InnerClassNode(cn, cn.getName() + "$CollectorHelper",
ACC_PUBLIC | ACC_STATIC | ACC_FINAL, ClassHelper.OBJECT_TYPE.getPlainNodeReference());
cn.getModule().addClass(helper);
helper.addAnnotation(new AnnotationNode(COMPILESTATIC_CLASSNODE));
collector.setMember("serializeClass", new ClassExpression(helper.getPlainNodeReference()));
}
// add static value():Object[][] method
List<AnnotationNode> meta = getMeta(cn);
List<Expression> outer = new ArrayList<>(meta.size());
for (AnnotationNode an : meta) {
Expression serialized = serialize(an);
outer.add(serialized);
}
ArrayExpression ae = new ArrayExpression(ClassHelper.OBJECT_TYPE.makeArray(), outer);
Statement code = new ReturnStatement(ae);
ClassNodeUtils.addGeneratedMethod(helper, "value", ACC_PUBLIC | ACC_STATIC,
ClassHelper.OBJECT_TYPE.makeArray().makeArray(),
Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, code);
// remove annotations
for (ListIterator<AnnotationNode> it = cn.getAnnotations().listIterator(); it.hasNext(); ) {
AnnotationNode an = it.next();
if (an == collector || "java.lang.annotation".equals(an.getClassNode().getPackageName())) {
continue;
}
it.remove();
}
}
private Expression serialize(Expression e) {
if (e instanceof AnnotationConstantExpression) {
AnnotationConstantExpression ace = (AnnotationConstantExpression) e;
return serialize((AnnotationNode) ace.getValue());
}
if (e instanceof ListExpression) {
boolean annotationConstant = false;
ListExpression le = (ListExpression) e;
List<Expression> list = le.getExpressions();
List<Expression> newList = new ArrayList<>(list.size());
for (Expression exp: list) {
annotationConstant = annotationConstant || exp instanceof AnnotationConstantExpression;
newList.add(serialize(exp));
}
ClassNode type = ClassHelper.OBJECT_TYPE;
if (annotationConstant) type = type.makeArray();
return new ArrayExpression(type, newList);
}
if (e instanceof ConstantExpression) {
Object obj = ((ConstantExpression) e).getValue();
if (obj instanceof Enum) {
return propX(classX(obj.getClass()), e);
}
}
return e;
}
private Expression serialize(AnnotationNode an) {
ClassExpression type = new ClassExpression(an.getClassNode());
type.setSourcePosition(an.getClassNode());
MapExpression map = new MapExpression();
for (Map.Entry<String, Expression> entry : an.getMembers().entrySet()) {
Expression key = new ConstantExpression(entry.getKey());
Expression val = serialize(entry.getValue());
map.addMapEntryExpression(key, val);
}
return new ArrayExpression(ClassHelper.OBJECT_TYPE, Arrays.asList(type, map));
}
}
/**
* Adds a new syntax error to the source unit and then continues.
*
* @param message the message
* @param node the node for the error report
* @param source the source unit for the error report
*/
protected void addError(String message, ASTNode node, SourceUnit source) {
source.getErrorCollector().addErrorAndContinue(message, node, source);
}
private List<AnnotationNode> getTargetListFromValue(AnnotationNode collector, AnnotationNode aliasAnnotationUsage, SourceUnit source) {
Expression memberValue = collector.getMember("value");
if (memberValue == null) {
return Collections.emptyList();
}
if (!(memberValue instanceof ListExpression)) {
addError("Annotation collector expected a list of classes, but got a "+memberValue.getClass(), collector, source);
return Collections.emptyList();
}
ListExpression memberListExp = (ListExpression) memberValue;
List<Expression> memberList = memberListExp.getExpressions();
if (memberList.isEmpty()) {
return Collections.emptyList();
}
List<AnnotationNode> ret = new ArrayList<>();
for (Expression e : memberList) {
AnnotationNode toAdd = new AnnotationNode(e.getType());
toAdd.setSourcePosition(aliasAnnotationUsage);
ret.add(toAdd);
}
return ret;
}
private static List<AnnotationNode> getStoredTargetList(AnnotationNode aliasAnnotationUsage, SourceUnit source) {
ClassNode alias = aliasAnnotationUsage.getClassNode().redirect();
List<AnnotationNode> ret = getMeta(alias);
return copy(ret, aliasAnnotationUsage);
}
private static List<AnnotationNode> copy(List<AnnotationNode> orig, AnnotationNode aliasAnnotationUsage) {
if (orig.isEmpty()) return orig;
List<AnnotationNode> ret = new ArrayList<>(orig.size());
for (AnnotationNode an : orig) {
AnnotationNode newAn = new AnnotationNode(an.getClassNode());
copyMembers(an, newAn);
newAn.setSourcePosition(aliasAnnotationUsage);
ret.add(newAn);
}
return ret;
}
private static List<AnnotationNode> getTargetListFromAnnotations(ClassNode alias) {
List<AnnotationNode> annotations = alias.getAnnotations();
if (annotations.size() < 2) {
return Collections.emptyList();
}
List<AnnotationNode> ret = new ArrayList<>(annotations.size());
for (AnnotationNode an : annotations) {
ClassNode type = an.getClassNode();
if (type.getName().equals(AnnotationCollector.class.getName())
|| "java.lang.annotation".equals(type.getPackageName())
|| "org.apache.groovy.lang.annotation.Incubating".equals(type.getName())) continue;
AnnotationNode toAdd = new AnnotationNode(type);
copyMembers(an, toAdd);
ret.add(toAdd);
}
return ret;
}
private static void copyMembers(final AnnotationNode from, final AnnotationNode to) {
Map<String, Expression> members = from.getMembers();
copyMembers(members, to);
}
private static void copyMembers(final Map<String, Expression> members, final AnnotationNode to) {
for (Map.Entry<String, Expression> entry : members.entrySet()) {
to.addMember(entry.getKey(), entry.getValue());
}
}
private static List<AnnotationNode> getTargetListFromClass(final ClassNode alias) {
ClassNode cn = getSerializeClass(alias);
Class<?> c = cn.getTypeClass();
Object[][] data;
try {
Method m = c.getMethod("value");
if (!Modifier.isStatic(m.getModifiers()))
throw new NoSuchMethodException("non-static value()");
data = (Object[][]) m.invoke(null);
return makeListOfAnnotations(data);
} catch (NoSuchMethodException | ClassCastException e) {
throw new GroovyRuntimeException("Expecting static method `Object[][] value()`" +
" in " + cn.toString(false) + ". Was it compiled from a Java source?");
} catch (Exception e) {
throw new GroovyBugError(e);
}
}
// 2.5.3 and above gets from annotation attribute otherwise self
private static ClassNode getSerializeClass(final ClassNode alias) {
List<AnnotationNode> collectors = alias.getAnnotations(ClassHelper.make(AnnotationCollector.class));
if (!collectors.isEmpty()) {
assert collectors.size() == 1;
AnnotationNode collectorNode = collectors.get(0);
Expression serializeClass = collectorNode.getMember("serializeClass");
if (serializeClass instanceof ClassExpression) {
ClassNode serializeClassType = serializeClass.getType();
if (!serializeClassType.getName().equals(AnnotationCollector.class.getName())) {
return serializeClassType;
}
}
}
return alias;
}
private static List<AnnotationNode> makeListOfAnnotations(final Object[][] data) {
if (data.length == 0) {
return Collections.emptyList();
}
List<AnnotationNode> ret = new ArrayList<>(data.length);
for (Object[] inner : data) {
Class<?> anno = (Class<?>) inner[0];
AnnotationNode toAdd = new AnnotationNode(ClassHelper.make(anno));
ret.add(toAdd);
@SuppressWarnings("unchecked")
Map<String,Object> member = (Map<String, Object>) inner[1];
if (member.isEmpty()) {
continue;
}
Map<String, Expression> generated = new HashMap<>(member.size());
for (Map.Entry<String, Object> entry : member.entrySet()) {
generated.put(entry.getKey(), makeExpression(entry.getValue()));
}
copyMembers(generated, toAdd);
}
return ret;
}
private static Expression makeExpression(final Object o) {
if (o instanceof Class) {
return new ClassExpression(ClassHelper.make((Class<?>) o));
}
//TODO: value as Annotation here!
if (o instanceof Object[][]) {
List<AnnotationNode> annotations = makeListOfAnnotations((Object[][])o);
ListExpression le = new ListExpression();
for (AnnotationNode an : annotations) {
le.addExpression(new AnnotationConstantExpression(an));
}
return le;
} else if (o instanceof Object[]) {
ListExpression le = new ListExpression();
Object[] values = (Object[]) o;
for (Object val : values) {
le.addExpression(makeExpression(val));
}
return le;
}
return new ConstantExpression(o,true);
}
/**
* Returns a list of AnnotationNodes for the value attribute of the given
* AnnotationNode.
*
* @param collector the node containing the value member with the list
* @param source the source unit for error reporting
* @return a list of string constants
*/
protected List<AnnotationNode> getTargetAnnotationList(AnnotationNode collector, AnnotationNode aliasAnnotationUsage, SourceUnit source) {
List<AnnotationNode> stored = getStoredTargetList(aliasAnnotationUsage, source);
List<AnnotationNode> targetList = getTargetListFromValue(collector, aliasAnnotationUsage, source);
int size = targetList.size() + stored.size();
if (size == 0) {
return Collections.emptyList();
}
List<AnnotationNode> ret = new ArrayList<>(size);
ret.addAll(stored);
ret.addAll(targetList);
return ret;
}
/**
* Implementation method of the alias annotation processor. This method will
* get the list of annotations we aliased from the collector and adds it to
* aliasAnnotationUsage. The method will also map all members from
* aliasAnnotationUsage to the aliased nodes. Should a member stay unmapped,
* we will add an error. Further processing of those members is done by the
* annotations.
*
* @param collector reference to the annotation with {@link AnnotationCollector}
* @param aliasAnnotationUsage reference to the place of usage of the alias
* @param aliasAnnotated reference to the node that has been annotated by the alias
* @param source source unit for error reporting
* @return list of the new AnnotationNodes
*/
public List<AnnotationNode> visit(AnnotationNode collector, AnnotationNode aliasAnnotationUsage, AnnotatedNode aliasAnnotated, SourceUnit source) {
List<AnnotationNode> ret = getTargetAnnotationList(collector, aliasAnnotationUsage, source);
Set<String> unusedNames = new HashSet<>(aliasAnnotationUsage.getMembers().keySet());
for (AnnotationNode an: ret) {
for (String name : aliasAnnotationUsage.getMembers().keySet()) {
if (an.getClassNode().hasMethod(name, Parameter.EMPTY_ARRAY)) {
unusedNames.remove(name);
an.setMember(name, aliasAnnotationUsage.getMember(name));
}
}
}
if (!unusedNames.isEmpty()) {
String message = "Annotation collector got unmapped names "+unusedNames.toString()+".";
addError(message, aliasAnnotationUsage, source);
}
return ret;
}
}