blob: 85a1c9c36a292598e4b5067c10a2b151827cacfe [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.transform.AutoClone;
import groovy.transform.AutoCloneStyle;
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.ConstructorNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.VariableScope;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.EmptyStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.ast.tools.GenericsUtils;
import org.codehaus.groovy.classgen.VariableScopeVisitor;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.runtime.InvokerHelper;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;
import static org.apache.groovy.ast.tools.ClassNodeUtils.addGeneratedConstructor;
import static org.apache.groovy.ast.tools.ClassNodeUtils.addGeneratedMethod;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveType;
import static org.codehaus.groovy.ast.ClassHelper.make;
import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
import static org.codehaus.groovy.ast.tools.GeneralUtils.assignS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
import static org.codehaus.groovy.ast.tools.GeneralUtils.callSuperX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.castX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.closureX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.declS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.equalsNullX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstanceNonPropertyFields;
import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstancePropertyFields;
import static org.codehaus.groovy.ast.tools.GeneralUtils.ifElseS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.isInstanceOfX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.isOrImplements;
import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.nullX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
import static org.codehaus.groovy.ast.tools.GeneralUtils.params;
import static org.codehaus.groovy.ast.tools.GeneralUtils.propX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
import static org.codehaus.groovy.ast.tools.GeneralUtils.ternaryX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
/**
* Handles generation of code for the @AutoClone annotation.
*/
@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
public class AutoCloneASTTransformation extends AbstractASTTransformation {
static final Class MY_CLASS = AutoClone.class;
static final ClassNode MY_TYPE = make(MY_CLASS);
static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
private static final ClassNode CLONEABLE_TYPE = make(Cloneable.class);
private static final ClassNode BAOS_TYPE = make(ByteArrayOutputStream.class);
private static final ClassNode BAIS_TYPE = make(ByteArrayInputStream.class);
private static final ClassNode OOS_TYPE = make(ObjectOutputStream.class);
private static final ClassNode OIS_TYPE = make(ObjectInputStream.class);
private static final ClassNode INVOKER_TYPE = make(InvokerHelper.class);
public void visit(ASTNode[] nodes, SourceUnit source) {
init(nodes, source);
AnnotatedNode parent = (AnnotatedNode) nodes[1];
AnnotationNode anno = (AnnotationNode) nodes[0];
if (!MY_TYPE.equals(anno.getClassNode())) return;
if (parent instanceof ClassNode) {
ClassNode cNode = (ClassNode) parent;
if (!checkNotInterface(cNode, MY_TYPE_NAME)) return;
cNode.addInterface(CLONEABLE_TYPE);
boolean includeFields = memberHasValue(anno, "includeFields", true);
AutoCloneStyle style = getStyle(anno, "style");
List<String> excludes = getMemberStringList(anno, "excludes");
if (!checkPropertyList(cNode, excludes, "excludes", anno, MY_TYPE_NAME, includeFields)) return;
List<FieldNode> list = getInstancePropertyFields(cNode);
if (includeFields) {
list.addAll(getInstanceNonPropertyFields(cNode));
}
if (style == null) style = AutoCloneStyle.CLONE;
switch (style) {
case COPY_CONSTRUCTOR:
createCloneCopyConstructor(cNode, list, excludes);
break;
case SERIALIZATION:
createCloneSerialization(cNode);
break;
case SIMPLE:
createSimpleClone(cNode, list, excludes);
break;
default:
createClone(cNode, list, excludes);
break;
}
}
}
private void createCloneSerialization(ClassNode cNode) {
final BlockStatement body = new BlockStatement();
// def baos = new ByteArrayOutputStream()
final Expression baos = localVarX("baos");
body.addStatement(declS(baos, ctorX(BAOS_TYPE)));
// baos.withObjectOutputStream{ it.writeObject(this) }
MethodCallExpression writeObject = callX(castX(OOS_TYPE, varX("it")), "writeObject", varX("this"));
writeObject.setImplicitThis(false);
ClosureExpression writeClos = closureX(block(stmt(writeObject)));
writeClos.setVariableScope(new VariableScope());
body.addStatement(stmt(callX(baos, "withObjectOutputStream", args(writeClos))));
// def bais = new ByteArrayInputStream(baos.toByteArray())
final Expression bais = localVarX("bais");
body.addStatement(declS(bais, ctorX(BAIS_TYPE, args(callX(baos, "toByteArray")))));
// return bais.withObjectInputStream(getClass().classLoader){ (<type>) it.readObject() }
MethodCallExpression readObject = callX(castX(OIS_TYPE, varX("it")), "readObject");
readObject.setImplicitThis(false);
ClosureExpression readClos = closureX(block(stmt(castX(GenericsUtils.nonGeneric(cNode), readObject))));
readClos.setVariableScope(new VariableScope());
Expression classLoader = callX(callThisX("getClass"), "getClassLoader");
body.addStatement(returnS(callX(bais, "withObjectInputStream", args(classLoader, readClos))));
new VariableScopeVisitor(sourceUnit, true).visitClass(cNode);
ClassNode[] exceptions = {make(CloneNotSupportedException.class)};
addGeneratedMethod(cNode, "clone", ACC_PUBLIC, GenericsUtils.nonGeneric(cNode), Parameter.EMPTY_ARRAY, exceptions, body);
}
private static void createCloneCopyConstructor(ClassNode cNode, List<FieldNode> list, List<String> excludes) {
if (cNode.getDeclaredConstructors().isEmpty()) {
// add no-arg constructor
BlockStatement noArgBody = new BlockStatement();
noArgBody.addStatement(EmptyStatement.INSTANCE);
addGeneratedConstructor(cNode, ACC_PUBLIC, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, noArgBody);
}
boolean hasThisCons = false;
for (ConstructorNode consNode : cNode.getDeclaredConstructors()) {
Parameter[] parameters = consNode.getParameters();
if (parameters.length == 1 && parameters[0].getType().equals(cNode)) {
hasThisCons = true;
}
}
if (!hasThisCons) {
BlockStatement initBody = new BlockStatement();
Parameter initParam = param(GenericsUtils.nonGeneric(cNode), "other");
final Expression other = varX(initParam);
boolean hasParent = cNode.getSuperClass() != ClassHelper.OBJECT_TYPE;
if (hasParent) {
initBody.addStatement(stmt(ctorX(ClassNode.SUPER, other)));
}
for (FieldNode fieldNode : list) {
String name = fieldNode.getName();
if (excludes != null && excludes.contains(name)) continue;
ClassNode fieldType = fieldNode.getType();
Expression direct = propX(other, name);
Expression to = propX(varX("this"), name);
Statement assignDirect = assignS(to, direct);
Statement assignCloned = assignS(to, castX(fieldType, callCloneDirectX(direct)));
Statement assignClonedDynamic = assignS(to, castX(fieldType, callCloneDynamicX(direct)));
if (isCloneableType(fieldType)) {
initBody.addStatement(assignCloned);
} else if (!possiblyCloneable(fieldType)) {
initBody.addStatement(assignDirect);
} else {
initBody.addStatement(ifElseS(isInstanceOfX(direct, CLONEABLE_TYPE), assignClonedDynamic, assignDirect));
}
}
addGeneratedConstructor(cNode, ACC_PROTECTED, params(initParam), ClassNode.EMPTY_ARRAY, initBody);
}
ClassNode[] exceptions = {make(CloneNotSupportedException.class)};
addGeneratedMethod(cNode, "clone", ACC_PUBLIC, GenericsUtils.nonGeneric(cNode), Parameter.EMPTY_ARRAY, exceptions, block(stmt(ctorX(cNode, args(varX("this"))))));
}
private static boolean isCloneableType(ClassNode fieldType) {
return isOrImplements(fieldType, CLONEABLE_TYPE) || !fieldType.getAnnotations(MY_TYPE).isEmpty();
}
private static boolean possiblyCloneable(ClassNode type) {
return !isPrimitiveType(type) && ((isCloneableType(type) || (type.getModifiers() & ACC_FINAL) == 0));
}
private static Expression callCloneDynamicX(Expression target) {
return callX(INVOKER_TYPE, "invokeMethod", args(target, constX("clone"), nullX()));
}
private static Expression callCloneDirectX(Expression direct) {
return ternaryX(equalsNullX(direct), nullX(), callX(direct, "clone"));
}
private static void createSimpleClone(ClassNode cNode, List<FieldNode> fieldNodes, List<String> excludes) {
if (cNode.getDeclaredConstructors().isEmpty()) {
// add no-arg constructor
addGeneratedConstructor(cNode, ACC_PUBLIC, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, block(EmptyStatement.INSTANCE));
}
addSimpleCloneHelperMethod(cNode, fieldNodes, excludes);
final Expression result = localVarX("_result");
ClassNode[] exceptions = {make(CloneNotSupportedException.class)};
addGeneratedMethod(cNode, "clone", ACC_PUBLIC, GenericsUtils.nonGeneric(cNode), Parameter.EMPTY_ARRAY, exceptions, block(
declS(result, ctorX(cNode)),
stmt(callThisX("cloneOrCopyMembers", args(result))),
returnS(result)));
}
private static void addSimpleCloneHelperMethod(ClassNode cNode, List<FieldNode> fieldNodes, List<String> excludes) {
Parameter methodParam = new Parameter(GenericsUtils.nonGeneric(cNode), "other");
final Expression other = varX(methodParam);
boolean hasParent = cNode.getSuperClass() != ClassHelper.OBJECT_TYPE;
BlockStatement methodBody = new BlockStatement();
if (hasParent) {
methodBody.addStatement(stmt(callSuperX("cloneOrCopyMembers", args(other))));
}
for (FieldNode fieldNode : fieldNodes) {
String name = fieldNode.getName();
if (excludes != null && excludes.contains(name)) continue;
ClassNode fieldType = fieldNode.getType();
Expression direct = propX(varX("this"), name);
Expression to = propX(other, name);
Statement assignDirect = assignS(to, direct);
Statement assignCloned = assignS(to, castX(fieldType, callCloneDirectX(direct)));
Statement assignClonedDynamic = assignS(to, castX(fieldType, callCloneDynamicX(direct)));
if (isCloneableType(fieldType)) {
methodBody.addStatement(assignCloned);
} else if (!possiblyCloneable(fieldType)) {
methodBody.addStatement(assignDirect);
} else {
methodBody.addStatement(ifElseS(isInstanceOfX(direct, CLONEABLE_TYPE), assignClonedDynamic, assignDirect));
}
}
ClassNode[] exceptions = {make(CloneNotSupportedException.class)};
addGeneratedMethod(cNode, "cloneOrCopyMembers", ACC_PROTECTED, ClassHelper.VOID_TYPE, params(methodParam), exceptions, methodBody);
}
private static void createClone(ClassNode cNode, List<FieldNode> fieldNodes, List<String> excludes) {
final BlockStatement body = new BlockStatement();
// def _result = super.clone() as cNode
final Expression result = localVarX("_result");
body.addStatement(declS(result, castX(cNode, callSuperX("clone"))));
for (FieldNode fieldNode : fieldNodes) {
if (excludes != null && excludes.contains(fieldNode.getName())) continue;
ClassNode fieldType = fieldNode.getType();
Expression fieldExpr = varX(fieldNode);
Expression to = propX(result, fieldNode.getName());
Statement doClone = assignS(to, castX(fieldType, callCloneDirectX(fieldExpr)));
Statement doCloneDynamic = assignS(to, castX(fieldType, callCloneDynamicX(fieldExpr)));
if (isCloneableType(fieldType)) {
body.addStatement(doClone);
} else if (possiblyCloneable(fieldType)) {
body.addStatement(ifS(isInstanceOfX(fieldExpr, CLONEABLE_TYPE), doCloneDynamic));
}
}
// return _result
body.addStatement(returnS(result));
ClassNode[] exceptions = {make(CloneNotSupportedException.class)};
addGeneratedMethod(cNode, "clone", ACC_PUBLIC, GenericsUtils.nonGeneric(cNode), Parameter.EMPTY_ARRAY, exceptions, body);
}
private static AutoCloneStyle getStyle(AnnotationNode node, String name) {
final Expression member = node.getMember(name);
if (member instanceof PropertyExpression) {
PropertyExpression prop = (PropertyExpression) member;
Expression oe = prop.getObjectExpression();
if (oe instanceof ClassExpression) {
ClassExpression ce = (ClassExpression) oe;
if (ce.getType().getName().equals("groovy.transform.AutoCloneStyle")) {
return AutoCloneStyle.valueOf(prop.getPropertyAsString());
}
}
}
return null;
}
}