Java tutorial
/* * Copyright 2008 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.transform; import groovy.lang.Immutable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; 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.MethodNode; import org.codehaus.groovy.ast.Parameter; import org.codehaus.groovy.ast.PropertyNode; import org.codehaus.groovy.ast.expr.ArgumentListExpression; import org.codehaus.groovy.ast.expr.BinaryExpression; import org.codehaus.groovy.ast.expr.BooleanExpression; import org.codehaus.groovy.ast.expr.CastExpression; import org.codehaus.groovy.ast.expr.ClassExpression; import org.codehaus.groovy.ast.expr.ConstantExpression; import org.codehaus.groovy.ast.expr.ConstructorCallExpression; import org.codehaus.groovy.ast.expr.DeclarationExpression; import org.codehaus.groovy.ast.expr.Expression; import org.codehaus.groovy.ast.expr.FieldExpression; import org.codehaus.groovy.ast.expr.MapExpression; import org.codehaus.groovy.ast.expr.MethodCallExpression; import org.codehaus.groovy.ast.expr.PropertyExpression; import org.codehaus.groovy.ast.expr.StaticMethodCallExpression; import org.codehaus.groovy.ast.expr.TupleExpression; import org.codehaus.groovy.ast.expr.VariableExpression; import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.EmptyStatement; import org.codehaus.groovy.ast.stmt.ExpressionStatement; import org.codehaus.groovy.ast.stmt.IfStatement; import org.codehaus.groovy.ast.stmt.ReturnStatement; import org.codehaus.groovy.ast.stmt.Statement; import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.runtime.DefaultGroovyMethods; import org.codehaus.groovy.syntax.Token; import org.codehaus.groovy.syntax.Types; import org.codehaus.groovy.util.HashCodeHelper; import org.objectweb.asm.Opcodes; /** * Handles generation of code for the @Immutable annotation. * This is experimental, use at your own risk. * * @author Paul King */ @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) public class ImmutableASTTransformation implements ASTTransformation, Opcodes { /* Currently leaving BigInteger and BigDecimal in list but see: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6348370 Also, Color is not final so while not normally used with child classes, it isn't strictly immutable. Use at your own risk. */ private static Class[] immutableList = { Boolean.class, Byte.class, Character.class, Double.class, Float.class, Integer.class, Long.class, Short.class, String.class, java.math.BigInteger.class, java.math.BigDecimal.class, java.awt.Color.class, }; private static final Class MY_CLASS = Immutable.class; private static final ClassNode MY_TYPE = new ClassNode(MY_CLASS); private static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage(); private static final ClassNode OBJECT_TYPE = new ClassNode(Object.class); private static final ClassNode HASHMAP_TYPE = new ClassNode(HashMap.class); private static final ClassNode MAP_TYPE = new ClassNode(Map.class); private static final ClassNode DATE_TYPE = new ClassNode(Date.class); private static final ClassNode CLONEABLE_TYPE = new ClassNode(Cloneable.class); private static final ClassNode COLLECTION_TYPE = new ClassNode(Collection.class); private static final ClassNode HASHUTIL_TYPE = new ClassNode(HashCodeHelper.class); private static final ClassNode STRINGBUFFER_TYPE = new ClassNode(StringBuffer.class); private static final ClassNode DGM_TYPE = new ClassNode(DefaultGroovyMethods.class); private static final ClassNode SELF_TYPE = new ClassNode(ImmutableASTTransformation.class); private static final Token COMPARE_EQUAL = Token.newSymbol(Types.COMPARE_EQUAL, -1, -1); private static final Token COMPARE_NOT_EQUAL = Token.newSymbol(Types.COMPARE_NOT_EQUAL, -1, -1); private static final Token COMPARE_IDENTICAL = Token.newSymbol(Types.COMPARE_IDENTICAL, -1, -1); private static final Token ASSIGN = Token.newSymbol(Types.ASSIGN, -1, -1); public void visit(ASTNode[] nodes, SourceUnit source) { if (nodes.length != 2 || !(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof AnnotatedNode)) { throw new RuntimeException( "Internal error: expecting [AnnotationNode, AnnotatedNode] but got: " + Arrays.asList(nodes)); } AnnotatedNode parent = (AnnotatedNode) nodes[1]; AnnotationNode node = (AnnotationNode) nodes[0]; if (!MY_TYPE.equals(node.getClassNode())) return; List<PropertyNode> newNodes = new ArrayList<PropertyNode>(); if (parent instanceof ClassNode) { ClassNode cNode = (ClassNode) parent; String cName = cNode.getName(); if (cNode.isInterface()) { throw new RuntimeException("Error processing interface '" + cName + "'. " + MY_TYPE_NAME + " not allowed for interfaces."); } if ((cNode.getModifiers() & ACC_FINAL) == 0) { cNode.setModifiers(cNode.getModifiers() | ACC_FINAL); } final List<PropertyNode> pList = cNode.getProperties(); for (PropertyNode pNode : pList) { adjustPropertyForImmutability(pNode, newNodes); } for (PropertyNode pNode : newNodes) { pList.remove(pNode); addProperty(cNode, pNode); } final List<FieldNode> fList = cNode.getFields(); for (FieldNode fNode : fList) { ensureNotPublic(cName, fNode); } createConstructor(cNode); createHashCode(cNode); createEquals(cNode); createToString(cNode); } } private boolean hasDeclaredMethod(ClassNode cNode, String name, int argsCount) { List<MethodNode> ms = cNode.getDeclaredMethods(name); for (MethodNode m : ms) { Parameter[] paras = m.getParameters(); if (paras != null && paras.length == argsCount) { return true; } } return false; } private void ensureNotPublic(String cNode, FieldNode fNode) { String fName = fNode.getName(); // TODO: do we need to lock down things like: $ownClass if (fNode.isPublic() && !fName.contains("$")) { throw new RuntimeException( "Public field '" + fName + "' not allowed for " + MY_TYPE_NAME + " class '" + cNode + "'."); } } private void createHashCode(ClassNode cNode) { // make a public method if none exists otherwise try a private method with leading underscore boolean hasExistingHashCode = hasDeclaredMethod(cNode, "hashCode", 0); if (hasExistingHashCode && hasDeclaredMethod(cNode, "_hashCode", 0)) return; final FieldNode hashField = cNode.addField("$hash$code", ACC_PRIVATE | ACC_SYNTHETIC, ClassHelper.int_TYPE, null); final BlockStatement body = new BlockStatement(); final Expression hash = new FieldExpression(hashField); final List<PropertyNode> list = cNode.getProperties(); body.addStatement( new IfStatement(isZeroExpr(hash), calculateHashStatements(hash, list), new EmptyStatement())); body.addStatement(new ReturnStatement(hash)); cNode.addMethod(new MethodNode(hasExistingHashCode ? "_hashCode" : "hashCode", hasExistingHashCode ? ACC_PRIVATE : ACC_PUBLIC, ClassHelper.int_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body)); } private void createToString(ClassNode cNode) { // make a public method if none exists otherwise try a private method with leading underscore boolean hasExistingToString = hasDeclaredMethod(cNode, "toString", 0); if (hasExistingToString && hasDeclaredMethod(cNode, "_toString", 0)) return; final BlockStatement body = new BlockStatement(); final List<PropertyNode> list = cNode.getProperties(); // def _result = new StringBuffer() final Expression result = new VariableExpression("_result"); final Expression init = new ConstructorCallExpression(STRINGBUFFER_TYPE, MethodCallExpression.NO_ARGUMENTS); body.addStatement(new ExpressionStatement(new DeclarationExpression(result, ASSIGN, init))); body.addStatement(append(result, new ConstantExpression(cNode.getName()))); body.addStatement(append(result, new ConstantExpression("("))); boolean first = true; for (PropertyNode pNode : list) { if (first) { first = false; } else { body.addStatement(append(result, new ConstantExpression(", "))); } body.addStatement( new IfStatement(new BooleanExpression(new FieldExpression(cNode.getField("$map$constructor"))), toStringPropertyName(result, pNode.getName()), new EmptyStatement())); final FieldExpression fieldExpr = new FieldExpression(pNode.getField()); body.addStatement(append(result, new MethodCallExpression(fieldExpr, "toString", MethodCallExpression.NO_ARGUMENTS))); } body.addStatement(append(result, new ConstantExpression(")"))); body.addStatement(new ReturnStatement( new MethodCallExpression(result, "toString", MethodCallExpression.NO_ARGUMENTS))); cNode.addMethod(new MethodNode(hasExistingToString ? "_toString" : "toString", hasExistingToString ? ACC_PRIVATE : ACC_PUBLIC, ClassHelper.STRING_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body)); } private Statement toStringPropertyName(Expression result, String fName) { final BlockStatement body = new BlockStatement(); body.addStatement(append(result, new ConstantExpression(fName))); body.addStatement(append(result, new ConstantExpression(":"))); return body; } private ExpressionStatement append(Expression result, Expression expr) { return new ExpressionStatement(new MethodCallExpression(result, "append", expr)); } private Statement calculateHashStatements(Expression hash, List<PropertyNode> list) { final BlockStatement body = new BlockStatement(); // def _result = HashCodeHelper.initHash() final Expression result = new VariableExpression("_result"); final Expression init = new StaticMethodCallExpression(HASHUTIL_TYPE, "initHash", MethodCallExpression.NO_ARGUMENTS); body.addStatement(new ExpressionStatement(new DeclarationExpression(result, ASSIGN, init))); // fields for (PropertyNode pNode : list) { // _result = HashCodeHelper.updateHash(_result, field) final Expression fieldExpr = new FieldExpression(pNode.getField()); final Expression args = new TupleExpression(result, fieldExpr); final Expression current = new StaticMethodCallExpression(HASHUTIL_TYPE, "updateHash", args); body.addStatement(assignStatement(result, current)); } // $hash$code = _result body.addStatement(assignStatement(hash, result)); return body; } private void createEquals(ClassNode cNode) { // make a public method if none exists otherwise try a private method with leading underscore boolean hasExistingEquals = hasDeclaredMethod(cNode, "equals", 1); if (hasExistingEquals && hasDeclaredMethod(cNode, "_equals", 1)) return; final BlockStatement body = new BlockStatement(); Expression other = new VariableExpression("other"); // some short circuit cases for efficiency body.addStatement(returnFalseIfNull(other)); body.addStatement(returnFalseIfWrongType(cNode, other)); body.addStatement(returnTrueIfIdentical(VariableExpression.THIS_EXPRESSION, other)); final List<PropertyNode> list = cNode.getProperties(); // fields for (PropertyNode pNode : list) { body.addStatement(returnFalseIfPropertyNotEqual(pNode, other)); } // default body.addStatement(new ReturnStatement(ConstantExpression.TRUE)); Parameter[] params = { new Parameter(OBJECT_TYPE, "other") }; cNode.addMethod(new MethodNode(hasExistingEquals ? "_equals" : "equals", hasExistingEquals ? ACC_PRIVATE : ACC_PUBLIC, ClassHelper.boolean_TYPE, params, ClassNode.EMPTY_ARRAY, body)); } private Statement returnFalseIfWrongType(ClassNode cNode, Expression other) { return new IfStatement(notEqualClasses(cNode, other), new ReturnStatement(ConstantExpression.FALSE), new EmptyStatement()); } private IfStatement returnFalseIfNull(Expression other) { return new IfStatement(equalsNullExpr(other), new ReturnStatement(ConstantExpression.FALSE), new EmptyStatement()); } private IfStatement returnTrueIfIdentical(Expression self, Expression other) { return new IfStatement(identicalExpr(self, other), new ReturnStatement(ConstantExpression.TRUE), new EmptyStatement()); } private Statement returnFalseIfPropertyNotEqual(PropertyNode pNode, Expression other) { return new IfStatement(notEqualsExpr(pNode, other), new ReturnStatement(ConstantExpression.FALSE), new EmptyStatement()); } private void addProperty(ClassNode cNode, PropertyNode pNode) { final FieldNode fn = pNode.getField(); cNode.getFields().remove(fn); cNode.addProperty(pNode.getName(), pNode.getModifiers() | ACC_FINAL, pNode.getType(), pNode.getInitialExpression(), pNode.getGetterBlock(), pNode.getSetterBlock()); final FieldNode newfn = cNode.getField(fn.getName()); cNode.getFields().remove(newfn); cNode.addField(fn); } private void createConstructor(ClassNode cNode) { // pretty toString will remember how the user declared the params and print accordingly final FieldNode constructorField = cNode.addField("$map$constructor", ACC_PRIVATE | ACC_SYNTHETIC, ClassHelper.boolean_TYPE, null); final FieldExpression constructorStyle = new FieldExpression(constructorField); if (cNode.getDeclaredConstructors().size() != 0) { // TODO: allow constructors which call provided constructor? throw new RuntimeException("Explicit constructors not allowed for " + MY_TYPE_NAME + " class: " + cNode.getNameWithoutPackage()); } List<PropertyNode> list = cNode.getProperties(); boolean specialHashMapCase = list.size() == 1 && list.get(0).getField().getType().equals(HASHMAP_TYPE); if (specialHashMapCase) { createConstructorMapSpecial(cNode, constructorStyle, list); } else { createConstructorMap(cNode, constructorStyle, list); createConstructorOrdered(cNode, constructorStyle, list); } } private void createConstructorMapSpecial(ClassNode cNode, FieldExpression constructorStyle, List<PropertyNode> list) { final BlockStatement body = new BlockStatement(); body.addStatement(createConstructorStatementMapSpecial(list.get(0).getField())); createConstructorMapCommon(cNode, constructorStyle, body); } private void createConstructorMap(ClassNode cNode, FieldExpression constructorStyle, List<PropertyNode> list) { final BlockStatement body = new BlockStatement(); for (PropertyNode pNode : list) { body.addStatement(createConstructorStatement(cNode, pNode)); } createConstructorMapCommon(cNode, constructorStyle, body); } private void createConstructorMapCommon(ClassNode cNode, FieldExpression constructorStyle, BlockStatement body) { final List<FieldNode> fList = cNode.getFields(); for (FieldNode fNode : fList) { if (!fNode.isPublic() && !fNode.getName().contains("$") && (cNode.getProperty(fNode.getName()) == null)) { body.addStatement(createConstructorStatementDefault(fNode)); } } body.addStatement(assignStatement(constructorStyle, ConstantExpression.TRUE)); final Parameter[] params = new Parameter[] { new Parameter(HASHMAP_TYPE, "args") }; cNode.addConstructor(new ConstructorNode(ACC_PUBLIC, params, ClassNode.EMPTY_ARRAY, new IfStatement(equalsNullExpr(new VariableExpression("args")), new EmptyStatement(), body))); } private void createConstructorOrdered(ClassNode cNode, FieldExpression constructorStyle, List<PropertyNode> list) { final MapExpression argMap = new MapExpression(); final Parameter[] orderedParams = new Parameter[list.size()]; int index = 0; for (PropertyNode pNode : list) { orderedParams[index++] = new Parameter(pNode.getField().getType(), pNode.getField().getName()); argMap.addMapEntryExpression(new ConstantExpression(pNode.getName()), new VariableExpression(pNode.getName())); } final BlockStatement orderedBody = new BlockStatement(); orderedBody.addStatement(new ExpressionStatement(new ConstructorCallExpression(ClassNode.THIS, new ArgumentListExpression(new CastExpression(HASHMAP_TYPE, argMap))))); orderedBody.addStatement(assignStatement(constructorStyle, ConstantExpression.FALSE)); cNode.addConstructor(new ConstructorNode(ACC_PUBLIC, orderedParams, ClassNode.EMPTY_ARRAY, orderedBody)); } private Statement createConstructorStatement(ClassNode cNode, PropertyNode pNode) { FieldNode fNode = pNode.getField(); final ClassNode fieldType = fNode.getType(); final Statement statement; if (fieldType.isArray() || implementsInterface(fieldType, CLONEABLE_TYPE)) { statement = createConstructorStatementArrayOrCloneable(fNode); } else if (fieldType.isDerivedFrom(DATE_TYPE)) { statement = createConstructorStatementDate(fNode); } else if (fieldType.isDerivedFrom(COLLECTION_TYPE) || fieldType.isDerivedFrom(MAP_TYPE)) { statement = createConstructorStatementCollection(fNode); } else if (isKnownImmutable(fieldType)) { statement = createConstructorStatementDefault(fNode); } else if (fieldType.isResolved()) { throw new RuntimeException( createErrorMessage(cNode.getName(), fNode.getName(), fieldType.getName(), "compiling")); } else { statement = createConstructorStatementGuarded(cNode, fNode); } return statement; } private boolean implementsInterface(ClassNode fieldType, ClassNode interfaceType) { return Arrays.asList(fieldType.getInterfaces()).contains(interfaceType); } private Statement createConstructorStatementGuarded(ClassNode cNode, FieldNode fNode) { final FieldExpression fieldExpr = new FieldExpression(fNode); Expression initExpr = fNode.getInitialValueExpression(); if (initExpr == null) initExpr = ConstantExpression.NULL; Expression unknown = findArg(fNode.getName()); return new IfStatement(equalsNullExpr(unknown), new IfStatement(equalsNullExpr(initExpr), new EmptyStatement(), assignStatement(fieldExpr, checkUnresolved(cNode, fNode, initExpr))), assignStatement(fieldExpr, checkUnresolved(cNode, fNode, unknown))); } private Expression checkUnresolved(ClassNode cNode, FieldNode fNode, Expression value) { Expression args = new TupleExpression(new ConstantExpression(cNode.getName()), new ConstantExpression(fNode.getName()), value); return new StaticMethodCallExpression(SELF_TYPE, "checkImmutable", args); } private Statement createConstructorStatementCollection(FieldNode fNode) { final FieldExpression fieldExpr = new FieldExpression(fNode); Expression initExpr = fNode.getInitialValueExpression(); if (initExpr == null) initExpr = ConstantExpression.NULL; Expression collection = findArg(fNode.getName()); return new IfStatement(equalsNullExpr(collection), new IfStatement(equalsNullExpr(initExpr), new EmptyStatement(), assignStatement(fieldExpr, cloneCollectionExpr(initExpr))), assignStatement(fieldExpr, cloneCollectionExpr(collection))); } private Statement createConstructorStatementMapSpecial(FieldNode fNode) { final FieldExpression fieldExpr = new FieldExpression(fNode); Expression initExpr = fNode.getInitialValueExpression(); if (initExpr == null) initExpr = ConstantExpression.NULL; Expression namedArgs = findArg(fNode.getName()); Expression baseArgs = new VariableExpression("args"); return new IfStatement(equalsNullExpr(baseArgs), new IfStatement(equalsNullExpr(initExpr), new EmptyStatement(), assignStatement(fieldExpr, cloneCollectionExpr(initExpr))), new IfStatement(equalsNullExpr(namedArgs), new IfStatement( isTrueExpr(new MethodCallExpression(baseArgs, "containsKey", new ConstantExpression(fNode.getName()))), assignStatement(fieldExpr, namedArgs), assignStatement(fieldExpr, cloneCollectionExpr(baseArgs))), new IfStatement( isOneExpr(new MethodCallExpression(baseArgs, "size", MethodCallExpression.NO_ARGUMENTS)), assignStatement(fieldExpr, cloneCollectionExpr(namedArgs)), assignStatement(fieldExpr, cloneCollectionExpr(baseArgs))))); } private boolean isKnownImmutable(ClassNode fieldType) { if (!fieldType.isResolved()) return false; // FIXASC (groovychange) cannot access typeclass for JDTClassNodes // old // Class typeClass = fieldType.getTypeClass(); // return typeClass.isEnum() || // typeClass.isPrimitive() || // inImmutableList(typeClass); String s = fieldType.getName(); return fieldType.isPrimitive() || fieldType.isEnum() || inImmutableList(fieldType.getName()); // FIXASC (groovychange) end } // FIXASC (groovychange) private static boolean inImmutableList(String signature) { for (int i = 0; i < immutableList.length; i++) { if (immutableList[i].getName().equals(signature)) { return true; } } return false; } // FIXASC (groovychange) end private static boolean inImmutableList(Class typeClass) { return Arrays.asList(immutableList).contains(typeClass); } private Statement createConstructorStatementDefault(FieldNode fNode) { final FieldExpression fieldExpr = new FieldExpression(fNode); Expression initExpr = fNode.getInitialValueExpression(); if (initExpr == null) initExpr = ConstantExpression.NULL; Expression value = findArg(fNode.getName()); return new IfStatement(equalsNullExpr(value), new IfStatement(equalsNullExpr(initExpr), new EmptyStatement(), assignStatement(fieldExpr, initExpr)), assignStatement(fieldExpr, value)); } private Statement createConstructorStatementArrayOrCloneable(FieldNode fNode) { final FieldExpression fieldExpr = new FieldExpression(fNode); Expression initExpr = fNode.getInitialValueExpression(); if (initExpr == null) initExpr = ConstantExpression.NULL; final Expression array = findArg(fNode.getName()); return new IfStatement(equalsNullExpr(array), new IfStatement(equalsNullExpr(initExpr), assignStatement(fieldExpr, ConstantExpression.NULL), assignStatement(fieldExpr, cloneArrayOrCloneableExpr(initExpr))), assignStatement(fieldExpr, cloneArrayOrCloneableExpr(array))); } private Statement createConstructorStatementDate(FieldNode fNode) { final FieldExpression fieldExpr = new FieldExpression(fNode); Expression initExpr = fNode.getInitialValueExpression(); if (initExpr == null) initExpr = ConstantExpression.NULL; final Expression date = findArg(fNode.getName()); return new IfStatement(equalsNullExpr(date), new IfStatement(equalsNullExpr(initExpr), assignStatement(fieldExpr, ConstantExpression.NULL), assignStatement(fieldExpr, cloneDateExpr(initExpr))), assignStatement(fieldExpr, cloneDateExpr(date))); } private Expression cloneDateExpr(Expression origDate) { return new ConstructorCallExpression(DATE_TYPE, new MethodCallExpression(origDate, "getTime", MethodCallExpression.NO_ARGUMENTS)); } private Statement assignStatement(Expression fieldExpr, Expression value) { return new ExpressionStatement(assignExpr(fieldExpr, value)); } private Expression assignExpr(Expression fieldExpr, Expression value) { return new BinaryExpression(fieldExpr, ASSIGN, value); } private BooleanExpression equalsNullExpr(Expression argExpr) { return new BooleanExpression(new BinaryExpression(argExpr, COMPARE_EQUAL, ConstantExpression.NULL)); } private BooleanExpression isTrueExpr(Expression argExpr) { return new BooleanExpression(new BinaryExpression(argExpr, COMPARE_EQUAL, ConstantExpression.TRUE)); } private BooleanExpression isZeroExpr(Expression expr) { return new BooleanExpression(new BinaryExpression(expr, COMPARE_EQUAL, new ConstantExpression(0))); } private BooleanExpression isOneExpr(Expression expr) { return new BooleanExpression(new BinaryExpression(expr, COMPARE_EQUAL, new ConstantExpression(1))); } private BooleanExpression notEqualsExpr(PropertyNode pNode, Expression other) { final Expression fieldExpr = new FieldExpression(pNode.getField()); final Expression otherExpr = new PropertyExpression(other, pNode.getField().getName()); return new BooleanExpression(new BinaryExpression(fieldExpr, COMPARE_NOT_EQUAL, otherExpr)); } private BooleanExpression identicalExpr(Expression self, Expression other) { return new BooleanExpression(new BinaryExpression(self, COMPARE_IDENTICAL, other)); } private BooleanExpression notEqualClasses(ClassNode cNode, Expression other) { return new BooleanExpression(new BinaryExpression(new ClassExpression(cNode), COMPARE_NOT_EQUAL, new MethodCallExpression(other, "getClass", MethodCallExpression.NO_ARGUMENTS))); } private Expression findArg(String fName) { return new PropertyExpression(new VariableExpression("args"), fName); } private void adjustPropertyForImmutability(PropertyNode pNode, List<PropertyNode> newNodes) { final FieldNode fNode = pNode.getField(); fNode.setModifiers((pNode.getModifiers() & (~ACC_PUBLIC)) | ACC_FINAL | ACC_PRIVATE); adjustPropertyNode(pNode, createGetterBody(fNode)); newNodes.add(pNode); } private void adjustPropertyNode(PropertyNode pNode, Statement getterBody) { pNode.setSetterBlock(null); pNode.setGetterBlock(getterBody); } private Statement createGetterBody(FieldNode fNode) { BlockStatement body = new BlockStatement(); final ClassNode fieldType = fNode.getType(); final Statement statement; if (fieldType.isArray() || implementsInterface(fieldType, CLONEABLE_TYPE)) { statement = createGetterBodyArrayOrCloneable(fNode); } else if (fieldType.isDerivedFrom(DATE_TYPE)) { statement = createGetterBodyDate(fNode); } else { statement = createGetterBodyDefault(fNode); } body.addStatement(statement); return body; } private Statement createGetterBodyDefault(FieldNode fNode) { final Expression fieldExpr = new FieldExpression(fNode); return new ExpressionStatement(fieldExpr); } private static String createErrorMessage(String className, String fieldName, String typeName, String mode) { return MY_TYPE_NAME + " processor doesn't know how to handle field '" + fieldName + "' of type '" + prettyTypeName(typeName) + "' while " + mode + " class " + className + ".\n" + MY_TYPE_NAME + " classes currently only support properties with known immutable types " + "or types where special handling achieves immutable behavior, including:\n" + "- Strings, primitive types, wrapper types, BigInteger and BigDecimal\n" + "- enums, other " + MY_TYPE_NAME + " classes and known immutables (java.awt.Color)\n" + "- Cloneable classes, collections, maps and arrays, and other classes with special handling (java.util.Date)\n" + "Other restrictions apply, please see the groovydoc for " + MY_TYPE_NAME + " for further details"; } private static String prettyTypeName(String name) { return name.equals("java.lang.Object") ? name + " or def" : name; } private Statement createGetterBodyArrayOrCloneable(FieldNode fNode) { final Expression fieldExpr = new FieldExpression(fNode); final Expression expression = cloneArrayOrCloneableExpr(fieldExpr); return safeExpression(fieldExpr, expression); } private Expression cloneArrayOrCloneableExpr(Expression fieldExpr) { return new MethodCallExpression(fieldExpr, "clone", MethodCallExpression.NO_ARGUMENTS); } private Expression cloneCollectionExpr(Expression fieldExpr) { return new StaticMethodCallExpression(DGM_TYPE, "asImmutable", fieldExpr); } private Statement createGetterBodyDate(FieldNode fNode) { final Expression fieldExpr = new FieldExpression(fNode); final Expression expression = cloneDateExpr(fieldExpr); return safeExpression(fieldExpr, expression); } private Statement safeExpression(Expression fieldExpr, Expression expression) { return new IfStatement(equalsNullExpr(fieldExpr), new ExpressionStatement(fieldExpr), new ExpressionStatement(expression)); } private static Object checkImmutable(String className, String fieldName, Object field) { if (field == null || field instanceof Enum || inImmutableList(field.getClass())) return field; if (field instanceof Collection) return DefaultGroovyMethods.asImmutable((Collection) field); if (field.getClass().getAnnotation(MY_CLASS) != null) return field; final String typeName = field.getClass().getName(); throw new RuntimeException(createErrorMessage(className, fieldName, typeName, "constructing")); } }