org.codehaus.groovy.transform.ImmutableASTTransformation.java Source code

Java tutorial

Introduction

Here is the source code for org.codehaus.groovy.transform.ImmutableASTTransformation.java

Source

/*
 * 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"));
    }

}