Index: src/main/groovy/transform/AutoCloneStyle.java
===================================================================
--- src/main/groovy/transform/AutoCloneStyle.java (revision )
+++ src/main/groovy/transform/AutoCloneStyle.java (revision )
@@ -0,0 +1,24 @@
+package groovy.transform;
+
+/**
+ * Intended style to use for cloning when using the {@code @}AutoClone annotation.
+ *
+ * @author Paul King
+ * @since 1.8.0
+ */
+public enum AutoCloneStyle {
+ /**
+ * Uses only cloning.
+ */
+ CLONE,
+
+ /**
+ * Uses the copy constructor pattern.
+ */
+ COPY_CONSTRUCTOR,
+
+ /**
+ * Uses serialization to clone.
+ */
+ SERIALIZATION
+}
Index: src/test/groovy/bugs/Groovy4121Bug.groovy
===================================================================
--- src/test/groovy/bugs/Groovy4121Bug.groovy (revision 19655)
+++ src/test/groovy/bugs/Groovy4121Bug.groovy (revision )
@@ -21,7 +21,7 @@
void testAssignmentToAFieldMadeFinalByImmutable() {
try {
new GroovyShell().parse """
- @Immutable
+ @groovy.transform.Immutable
class Account4121 {
BigDecimal balance
String customer
Index: src/test/org/codehaus/groovy/transform/CanonicalTransformTest.groovy
===================================================================
--- src/test/org/codehaus/groovy/transform/CanonicalTransformTest.groovy (revision )
+++ src/test/org/codehaus/groovy/transform/CanonicalTransformTest.groovy (revision )
@@ -0,0 +1,424 @@
+/*
+ * Copyright 2008-2010 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
+
+/**
+ * @author Paulo Poiati
+ * @author Paul King
+ */
+class CanonicalTransformTest extends GroovyShellTestCase {
+
+ void testCanonical() {
+ def objects = evaluate("""
+ import groovy.transform.Canonical
+ @Canonical class Foo {
+ String x, y
+ }
+ [new Foo(x:'x', y:'y'),
+ new Foo('x', 'y')]
+ """)
+
+ assertEquals objects[0].hashCode(), objects[1].hashCode()
+ assertEquals objects[0], objects[1]
+ }
+
+ void testCanonicalCantAlsoBeImmutable() {
+ def msg = shouldFail(RuntimeException) {
+ assertScript """
+ import groovy.transform.*
+ @Canonical
+ @Immutable
+ class Foo {
+ String bar
+ }
+ """
+ }
+ assert msg.contains("@Canonical class 'Foo' can't also be @Immutable")
+ }
+
+ void testCanonicalWithDeclaredConstructor() {
+ def msg = shouldFail(GroovyRuntimeException) {
+ assertScript """
+ @groovy.transform.Canonical class Foo {
+ def foo, bar, baz
+
+ Foo() {}
+
+ Foo(foo, bar) {
+ this.foo = foo
+ this.bar = bar
+ }
+ }
+
+ def foo = new Foo('a', 'b')
+ def foo1 = new Foo()
+ foo1.foo = 'a'
+ foo1.bar = 'b'
+ assert foo == foo1
+
+ // Fail here
+ new Foo('a', 'b', 'c')
+ """
+ }
+ assert msg.contains('Could not find matching constructor')
+ }
+
+ void testCanonicalNotCopyOrCloneProperty() {
+ assertScript """
+ def date = new Date()
+ def array = [1, 2, 3] as Integer[]
+ def map = [foo: 'bar']
+ def collection = [4, 5, 6]
+
+ @groovy.transform.Canonical class Foo {
+ Date date
+ Integer[] array
+ Map map
+ Collection collection
+ }
+
+ def foo = new Foo(date, array, map, collection)
+
+ assert date.is(foo.date)
+ assert array.is(foo.array)
+ assert map.is(foo.map)
+ assert collection.is(foo.collection)
+ """
+ }
+
+ void testCanonicalChange() {
+ def objects = evaluate("""
+ @groovy.transform.Canonical class Foo {
+ String x, y
+ }
+ [new Foo(x:'x', y:'y'),
+ new Foo('x', 'y')]
+ """)
+ objects[0].x = 'z'
+ assertFalse objects[0] == objects[1]
+ assertFalse objects[0].hashCode() == objects[1].hashCode()
+ }
+
+ void testUntyped() {
+ def object = evaluate("""
+ @groovy.transform.Canonical class Foo {
+ def x
+ def y = 10
+ }
+ new Foo()
+ """)
+
+ assert null == object.x
+ assert 10 == object.y
+
+ object.x = 20
+ assert 20 == object.x
+ object.x = [1, 2, 3]
+ assert [1, 2, 3] == object.x
+ }
+
+ void testMapConstructorOptional() {
+ assertScript """
+ @groovy.transform.Canonical class Foo {
+ String bar
+ String baz = 'a'
+ }
+
+ def foo = new Foo(bar: 'c')
+ def foo1 = new Foo(baz: 'd')
+ assert 'a' == foo.baz
+ assert 'c' == foo.bar
+ assert 'd' == foo1.baz
+ assert null == foo1.bar
+ """
+ }
+
+ void testMapConstructorOptionalPrimitive() {
+ assertScript """
+ @groovy.transform.Canonical class Foo {
+ String a
+ int b
+ char c
+ short d
+ long e
+ byte f
+ double h
+ float i
+ boolean j
+ }
+
+ new Foo("foo")
+ new Foo("foo", 10)
+ new Foo("foo", 10, (char) 20)
+ new Foo("foo", 10, (char) 20, (short) 30)
+ new Foo("foo", 10, (char) 20, (short) 30, 40L)
+ new Foo("foo", 10, (char) 20, (short) 30, 40L, (byte) 50)
+ new Foo("foo", 10, (char) 20, (short) 30, 40L, (byte) 50, 0.0)
+ new Foo("foo", 10, (char) 20, (short) 30, 40L, (byte) 50, 0.0, 0.0F)
+ """
+ }
+
+ void testOrderedConstructorOptional() {
+ assertScript """
+ @groovy.transform.Canonical class Foo {
+ String bar
+ String baz = 'a'
+ }
+
+ def foo = new Foo()
+ def foo0 = new Foo('c')
+ def foo1 = new Foo('c', 'd')
+ assert null == foo.bar
+ assert 'a' == foo.baz
+ assert 'c' == foo0.bar
+ assert 'a' == foo0.baz
+ assert 'c' == foo1.bar
+ assert 'd' == foo1.baz
+ """
+ }
+
+ void testCanonicalListProp() {
+ def objects = evaluate("""
+ @groovy.transform.Canonical class HasList {
+ String[] letters
+ List nums
+ }
+ def letters = 'A,B,C'.split(',')
+ def nums = [1, 2]
+ [new HasList(letters:letters, nums:nums),
+ new HasList(letters, nums)]
+ """)
+
+ assertEquals objects[0].hashCode(), objects[1].hashCode()
+ assertEquals objects[0], objects[1]
+ assert objects[0].letters.size() == 3
+ assert objects[0].nums.size() == 2
+
+ objects[0].nums = [1, 2, 3]
+ objects[1].letters = 'D,E'.split(',')
+
+ assert objects[0].nums.size() == 3
+ assert objects[1].letters.size() == 2
+ assertFalse objects[0] == objects[1]
+ assert !(objects[0].hashCode() == objects[1].hashCode())
+ }
+
+ void testCanonicalChangeArray() {
+ assertScript """
+ @groovy.transform.Canonical class HasListAndMap {
+ Object[] foo
+ }
+ def object = new HasListAndMap()
+ def object2 = new HasListAndMap(['bar'] as Object[])
+
+ assert object != object2
+
+ object.foo = new Object[1]
+ object.foo[0] = 'bar'
+
+ assert object == object2
+ """
+ }
+
+ void testCanonicalChangeCollection() {
+ def object = evaluate("""
+ @groovy.transform.Canonical class HasListAndMap {
+ List nums
+ Map map
+ }
+ new HasListAndMap(nums:[], map:[:])
+ """)
+
+ object.nums << 1
+ object.nums.add 2
+ object.map.foo = "bat"
+ assertEquals 2, object.nums.size()
+ assertEquals 1, object.map.size()
+ object.nums.remove 0
+ assertEquals 1, object.nums.size()
+ }
+
+ void testCanonicalAsMapKey() {
+ assertScript """
+ @groovy.transform.Canonical final class HasString {
+ String s
+ }
+ def k1 = new HasString('xyz')
+ def k2 = new HasString('xyz')
+ def map = [(k1):42]
+ assert map[k2] == 42
+ """
+ }
+
+ void testCanonicalWithOnlyMap() {
+ assertScript """
+ @groovy.transform.Canonical final class HasMap {
+ Map map
+ }
+ def m = new HasMap([:])
+ new HashMap()
+ """
+ }
+
+ void testCanonicalWithInvalidPropertyName() {
+ def msg = shouldFail(MissingPropertyException) {
+ assertScript """
+ @groovy.transform.Canonical class Simple { }
+ new Simple(missing:'Name')
+ """
+ }
+ assert msg.contains('No such property: missing for class: Simple')
+ }
+
+ void testCanonicalWithHashMap() {
+ assertScript """
+ @groovy.transform.Canonical final class HasHashMap {
+ HashMap map = [d:4]
+ }
+ assert new HasHashMap([a:1]).map == [a:1]
+ assert new HasHashMap(c:3).map == [c:3]
+ assert new HasHashMap(null).map == null
+ assert new HasHashMap().map == [d:4]
+ assert new HasHashMap([:]).map == [:]
+ assert new HasHashMap(map:5, c:3).map == [map:5, c:3]
+ assert new HasHashMap(map:[:]).map == [map:[:]]
+ """
+ }
+
+ void testCanonicalEquals() {
+ assertScript """
+ @groovy.transform.Canonical class This { String value }
+ @groovy.transform.Canonical class That { String value }
+ class Other { }
+
+ assert new This('foo') == new This("foo")
+ assert new This('f${"o"}o') == new This("foo")
+
+ assert new This('foo') != new This("bar")
+ assert new This('foo') != new That("foo")
+ assert new This('foo') != new Other()
+ assert new Other() != new This("foo")
+ """
+ }
+
+ void testExistingToString() {
+ assertScript """
+ import groovy.transform.Canonical
+ @Canonical class Foo {
+ String value
+ }
+ @Canonical class Bar {
+ String value
+ String toString() { 'zzz' + _toString() }
+ }
+ @Canonical class Baz {
+ String value
+ String toString() { 'zzz' + _toString() }
+ def _toString() { 'xxx' }
+ }
+ def foo = new Foo('abc')
+ def foo0 = new Foo('abc')
+ def foo1 = new Foo(value:'abc')
+ def bar = new Bar('abc')
+ def baz = new Baz('abc')
+ assert bar.toString() == 'zzz' + foo.toString().replaceAll('Foo', 'Bar')
+ assert baz.toString() == 'zzzxxx'
+
+ assert 'Foo(abc)' == foo0.toString()
+ foo0.value = 'cde'
+ assert 'Foo(cde)' == foo0.toString()
+ assert 'Foo(abc)' == foo1.toString()
+ foo1.value = 'cde'
+ assert 'Foo(cde)' == foo1.toString()
+ """
+ }
+
+ void testExistingEquals() {
+ assertScript """
+ import groovy.transform.Canonical
+ @Canonical class Foo {
+ String value
+ }
+ @Canonical class Bar {
+ String value
+ // doesn't follow normal conventions - for testing only
+ boolean equals(other) { value == 'abc' || _equals(other) }
+ }
+ @Canonical class Baz {
+ String value
+ // doesn't follow normal conventions - for testing only
+ boolean equals(Baz other) { value == 'abc' || _equals(other) }
+ def _equals(other) { false }
+ }
+ def foo1 = new Foo('abc')
+ def foo2 = new Foo('abc')
+ def foo3 = new Foo('def')
+ assert foo1 == foo2
+ assert foo1 != foo3
+
+ def bar1 = new Bar('abc')
+ def bar2 = new Bar('abc')
+ def bar3 = new Bar('def')
+ def bar4 = new Bar('def')
+ assert bar1 == bar2
+ assert bar1 == bar3
+ assert bar3 != bar1
+
+ def baz1 = new Baz('abc')
+ def baz2 = new Baz('abc')
+ def baz3 = new Baz('def')
+ def baz4 = new Baz('def')
+ assert baz1 == baz2
+ assert baz1 == baz3
+ assert baz3 != baz1
+ assert baz3 != baz4
+ """
+ }
+
+ void testExistingHashCode() {
+ assertScript """
+ import groovy.transform.Canonical
+ @Canonical class Foo {
+ String value
+ }
+ @Canonical class Bar {
+ String value
+ // doesn't follow normal conventions - for testing only
+ int hashCode() { value == 'abc' ? -1 : _hashCode() }
+ }
+ @Canonical class Baz {
+ String value
+ // doesn't follow normal conventions - for testing only
+ int hashCode() { value == 'abc' ? -1 : _hashCode() }
+ def _hashCode() { -100 }
+ }
+ def foo1 = new Foo('abc')
+ def foo2 = new Foo('abc')
+ assert foo1.hashCode() == foo2.hashCode()
+
+ def bar1 = new Bar('abc')
+ def bar2 = new Bar('def')
+ def bar3 = new Bar('def')
+ assert bar1.hashCode() == -1
+ assert bar2.hashCode() == bar3.hashCode()
+
+ def baz1 = new Baz('abc')
+ def baz2 = new Baz('def')
+ assert baz1.hashCode() == -1
+ assert baz2.hashCode() == -100
+ """
+ }
+}
Index: src/main/groovy/transform/ToString.java
===================================================================
--- src/main/groovy/transform/ToString.java (revision )
+++ src/main/groovy/transform/ToString.java (revision )
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2008-2010 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 groovy.transform;
+
+import org.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Class annotation used to assist in the creation of {@code toString()} methods in classes.
+ * The {@code @ToString} annotation instructs the compiler to execute an
+ * AST transformation which adds the necessary toString() method.
+ *
+ * It allows you to write classes in this shortened form:
+ *
+ * {@code @ToString}
+ * class Customer {
+ * String first, last
+ * int age
+ * Date since = new Date()
+ * Collection favItems
+ * private answer = 42
+ * }
+ * println new Customer(first:'Tom', last:'Jones', age:21, favItems:['Books', 'Games'])
+ *
+ * Which will have this output:
+ *
+ * Customer(Tom, Jones, 21, Wed Jul 14 23:57:14 EST 2010, [Books, Games])
+ *
+ * There are numerous options to customize the format of the generated output.
+ * E.g. if you change the first annotation to:
+ *
+ * {@code @ToString(includeNames=true)}
+ *
+ * Then the output will be:
+ *
+ * Customer(first:Tom, last:Jones, age:21, since:Wed Jul 14 23:57:50 EST 2010, favItems:[Books, Games])
+ *
+ * Or if you change the first annotation to:
+ *
+ * {@code @ToString(includeNames=true,includeFields=true,excludes="since,favItems")}
+ *
+ * Then the output will be:
+ *
+ * Customer(first:Tom, last:Jones, age:21, answer:42)
+ *
+ * If you have this example:
+ *
+ * import groovy.transform.ToString
+ * {@code @ToString} class NamedThing {
+ * String name
+ * }
+ * {@code @ToString}(includeNames=true,includeSuper=true)
+ * class AgedThing extends NamedThing {
+ * int age
+ * }
+ * println new AgedThing(name:'Lassie', age:5)
+ *
+ * Then the output will be:
+ *
+ * AgedThing(age:5, super:NamedThing(Lassie))
+ *
+ *
+ * @author Paul King
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.ToStringASTTransformation")
+public @interface ToString {
+ /**
+ * Comma separated list of field and property names to exclude from generated toString
+ */
+ String excludes() default "";
+
+ /**
+ * Whether to include super in generated toString
+ */
+ boolean includeSuper() default false;
+
+ /**
+ * Whether to include names of properties/fields in generated toString
+ */
+ boolean includeNames() default false;
+
+ /**
+ * Include fields as well as properties in generated toString
+ */
+ boolean includeFields() default false;
+}
Index: src/main/groovy/transform/TupleConstructor.java
===================================================================
--- src/main/groovy/transform/TupleConstructor.java (revision )
+++ src/main/groovy/transform/TupleConstructor.java (revision )
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2008-2010 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 groovy.transform;
+
+import org.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Class annotation used to assist in the creation of tuple constructors in classes.
+ *
+ * It allows you to write classes in this shortened form:
+ *
+ * {@code @TupleConstructor} class Customer {
+ * String first, last
+ * int age
+ * Date since
+ * Collection favItems
+ * }
+ * def c1 = new Customer(first:'Tom', last:'Jones', age:21, since:new Date(), favItems:['Books', 'Games'])
+ * def c2 = new Customer('Tom', 'Jones', 21, new Date(), ['Books', 'Games'])
+ * def c3 = new Customer('Tom', 'Jones')
+ *
+ * The {@code @TupleConstructor} annotation instructs the compiler to execute an
+ * AST transformation which adds the necessary toString() method.
+ *
+ * A tuple constructor is created with a parameter for each property (and optionally field).
+ * A default value is provided (using Java's default values) for all parameters in the constructor.
+ * Groovy then allows any number of parameters to be left off the end of the parameter list.
+ *
+ * @author Paul King
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.TupleConstructorASTTransformation")
+public @interface TupleConstructor {
+ /**
+ * Comma separated list of field and property names to exclude from the constructor
+ */
+ String excludes() default "";
+
+ /**
+ * Include fields as well as properties in the constructor
+ */
+ boolean includeFields() default false;
+
+ /**
+ * By default, this annotation becomes a no-op if you provide your own constructor.
+ * By setting {@code force=true} then the tuple constructor(s) will be added regardless of
+ * whether existing constructors exist. It is up to you to avoid creating duplicate constructors.
+ */
+ boolean force() default false;
+}
Index: src/main/org/codehaus/groovy/transform/AbstractASTTransformation.java
===================================================================
--- src/main/org/codehaus/groovy/transform/AbstractASTTransformation.java (revision )
+++ src/main/org/codehaus/groovy/transform/AbstractASTTransformation.java (revision )
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2008-2010 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 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.ClassNode;
+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.PropertyExpression;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
+import org.codehaus.groovy.syntax.SyntaxException;
+import org.objectweb.asm.Opcodes;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public abstract class AbstractASTTransformation implements Opcodes, ASTTransformation {
+ protected static final ClassNode SELF_TYPE = new ClassNode(ImmutableASTTransformation.class);
+ protected static final ClassNode HASHMAP_TYPE = new ClassNode(HashMap.class);
+ protected static final ClassNode MAP_TYPE = new ClassNode(Map.class);
+ private SourceUnit sourceUnit;
+
+ protected void init(ASTNode[] nodes, SourceUnit sourceUnit) {
+ if (nodes.length != 2 || !(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof AnnotatedNode)) {
+ throw new GroovyBugError("Internal error: expecting [AnnotationNode, AnnotatedNode] but got: " + Arrays.asList(nodes));
+ }
+ this.sourceUnit = sourceUnit;
+ }
+
+ protected boolean memberHasValue(AnnotationNode node, String name, Object value) {
+ final Expression member = node.getMember(name);
+ return member != null && member instanceof ConstantExpression && ((ConstantExpression) member).getValue().equals(value);
+ }
+
+ protected Object getMemberValue(AnnotationNode node, String name) {
+ final Expression member = node.getMember(name);
+ if (member != null && member instanceof ConstantExpression) return ((ConstantExpression) member).getValue();
+ return null;
+ }
+
+ protected void addError(String msg, ASTNode expr) {
+ int line = expr.getLineNumber();
+ int col = expr.getColumnNumber();
+ sourceUnit.getErrorCollector().addErrorAndContinue(
+ new SyntaxErrorMessage(new SyntaxException(msg + '\n', line, col), sourceUnit)
+ );
+ }
+
+ protected void checkNotInterface(ClassNode cNode, String annotationName) {
+ if (cNode.isInterface()) {
+ addError("Error processing interface '" + cNode.getName() + "'. " +
+ annotationName + " not allowed for interfaces.", cNode);
+ }
+ }
+
+ protected boolean hasAnnotation(ClassNode cNode, ClassNode annotation) {
+ List annots = cNode.getAnnotations(annotation);
+ return (annots != null && annots.size() > 0);
+ }
+}
Index: src/main/org/codehaus/groovy/transform/ToStringASTTransformation.java
===================================================================
--- src/main/org/codehaus/groovy/transform/ToStringASTTransformation.java (revision )
+++ src/main/org/codehaus/groovy/transform/ToStringASTTransformation.java (revision )
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2008-2010 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.transform.ToString;
+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.FieldNode;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.expr.BooleanExpression;
+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.MethodCallExpression;
+import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
+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.runtime.InvokerHelper;
+import org.codehaus.groovy.syntax.Token;
+import org.codehaus.groovy.syntax.Types;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstanceNonPropertyFields;
+import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstancePropertyFields;
+import static org.codehaus.groovy.transform.AbstractASTTransformUtil.hasDeclaredMethod;
+
+/**
+ * Handles generation of code for the @ToString annotation.
+ *
+ * @author Paul King
+ */
+@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
+public class ToStringASTTransformation extends AbstractASTTransformation {
+
+ static final Class MY_CLASS = ToString.class;
+ static final ClassNode MY_TYPE = new ClassNode(MY_CLASS);
+ static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
+ private static final ClassNode STRINGBUFFER_TYPE = new ClassNode(StringBuffer.class);
+ private static final ClassNode INVOKER_TYPE = new ClassNode(InvokerHelper.class);
+ private static final Token ASSIGN = Token.newSymbol(Types.ASSIGN, -1, -1);
+
+ 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;
+ checkNotInterface(cNode, MY_TYPE_NAME);
+ boolean includeSuper = memberHasValue(anno, "includeSuper", true);
+ if (includeSuper && cNode.getSuperClass().getName().equals("java.lang.Object")) {
+ addError("Error during " + MY_TYPE_NAME + " processing: includeSuper=true but '" + cNode.getName() + "' has no super class.", anno);
+ }
+ boolean includeNames = memberHasValue(anno, "includeNames", true);
+ boolean includeFields = memberHasValue(anno, "includeFields", true);
+ String rawExcludes = (String)getMemberValue(anno, "excludes");
+ List excludes = rawExcludes == null ? new ArrayList() : DefaultGroovyMethods.tokenize(rawExcludes, ", ");
+ toStringInit(cNode, new ConstantExpression(includeNames));
+ createToString(cNode, includeSuper, includeFields, excludes);
+ }
+ }
+
+ public static void createToString(ClassNode cNode, boolean includeSuper, boolean includeFields, List excludes) {
+ // 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();
+ // 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;
+ List list = getInstancePropertyFields(cNode);
+ if (includeFields) {
+ list.addAll(getInstanceNonPropertyFields(cNode));
+ }
+ for (FieldNode fNode : list) {
+ if (excludes.contains(fNode.getName()) || fNode.getName().contains("$")) continue;
+ first = appendPrefix(cNode, body, result, first, fNode.getName());
+ body.addStatement(append(result, new StaticMethodCallExpression(INVOKER_TYPE, "toString", new FieldExpression(fNode))));
+ }
+ if (includeSuper) {
+ appendPrefix(cNode, body, result, first, "super");
+ // not through MOP to avoid infinite recursion
+ body.addStatement(append(result, new MethodCallExpression(VariableExpression.SUPER_EXPRESSION, "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 static boolean appendPrefix(ClassNode cNode, BlockStatement body, Expression result, boolean first, String name) {
+ if (first) {
+ first = false;
+ } else {
+ body.addStatement(append(result, new ConstantExpression(", ")));
+ }
+ body.addStatement(new IfStatement(
+ new BooleanExpression(new FieldExpression(cNode.getField("$print$names"))),
+ toStringPropertyName(result, name),
+ new EmptyStatement()
+ ));
+ return first;
+ }
+
+ private static 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 static ExpressionStatement append(Expression result, Expression expr) {
+ return new ExpressionStatement(new MethodCallExpression(result, "append", expr));
+ }
+
+ public static void toStringInit(ClassNode cNode, ConstantExpression fieldValue) {
+ cNode.addField("$print$names", ACC_PRIVATE | ACC_SYNTHETIC, ClassHelper.boolean_TYPE, fieldValue);
+ }
+
+}
Index: src/main/org/codehaus/groovy/transform/AutoExternalizeASTTransformation.java
===================================================================
--- src/main/org/codehaus/groovy/transform/AutoExternalizeASTTransformation.java (revision )
+++ src/main/org/codehaus/groovy/transform/AutoExternalizeASTTransformation.java (revision )
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2008-2010 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.transform.AutoExternalize;
+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.FieldNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.expr.BinaryExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.FieldExpression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.ExpressionStatement;
+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 java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstanceNonPropertyFields;
+import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstancePropertyFields;
+
+/**
+ * Handles generation of code for the @AutoExternalize annotation.
+ *
+ * @author Paul King
+ */
+@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
+public class AutoExternalizeASTTransformation extends AbstractASTTransformation {
+ static final Class MY_CLASS = AutoExternalize.class;
+ static final ClassNode MY_TYPE = new ClassNode(MY_CLASS);
+ static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
+ private static final ClassNode EXTERNALIZABLE_TYPE = ClassHelper.make(Externalizable.class);
+ private static final ClassNode OBJECTOUTPUT_TYPE = ClassHelper.make(ObjectOutput.class);
+ private static final ClassNode OBJECTINPUT_TYPE = ClassHelper.make(ObjectInput.class);
+ private static final Token ASSIGN = Token.newSymbol(Types.ASSIGN, -1, -1);
+
+ 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;
+ checkNotInterface(cNode, MY_TYPE_NAME);
+ cNode.addInterface(EXTERNALIZABLE_TYPE);
+ boolean includeFields = memberHasValue(anno, "includeFields", true);
+ String rawExcludes = (String) getMemberValue(anno, "excludes");
+ List excludes = rawExcludes == null ? new ArrayList() : DefaultGroovyMethods.tokenize(rawExcludes, ", ");
+ List list = getInstancePropertyFields(cNode);
+ if (includeFields) {
+ list.addAll(getInstanceNonPropertyFields(cNode));
+ }
+ createWriteExternal(cNode, excludes, list);
+ createReadExternal(cNode, excludes, list);
+ }
+ }
+
+ private void createWriteExternal(ClassNode cNode, List excludes, List list) {
+ final BlockStatement body = new BlockStatement();
+ VariableExpression out = new VariableExpression("out", OBJECTOUTPUT_TYPE);
+ for (FieldNode fNode : list) {
+ if (excludes.contains(fNode.getName())) continue;
+ if ((fNode.getModifiers() & ACC_TRANSIENT) != 0) continue;
+ body.addStatement(new ExpressionStatement(new MethodCallExpression(out, "write" + suffixForField(fNode), new FieldExpression(fNode))));
+ }
+ ClassNode[] exceptions = {ClassHelper.make(IOException.class)};
+ cNode.addMethod("writeExternal", ACC_PUBLIC, ClassHelper.VOID_TYPE, new Parameter[]{new Parameter(OBJECTOUTPUT_TYPE, "out")}, exceptions, body);
+ }
+
+ private void createReadExternal(ClassNode cNode, List excludes, List list) {
+ final BlockStatement body = new BlockStatement();
+ final Expression oin = new VariableExpression("oin", OBJECTINPUT_TYPE);
+ for (FieldNode fNode : list) {
+ if (excludes.contains(fNode.getName())) continue;
+ if ((fNode.getModifiers() & ACC_TRANSIENT) != 0) continue;
+ Expression readObject = new MethodCallExpression(oin, "read" + suffixForField(fNode), MethodCallExpression.NO_ARGUMENTS);
+ body.addStatement(new ExpressionStatement(new BinaryExpression(new FieldExpression(fNode), ASSIGN, readObject)));
+ }
+ cNode.addMethod("readExternal", ACC_PUBLIC, ClassHelper.VOID_TYPE, new Parameter[]{new Parameter(OBJECTINPUT_TYPE, "oin")}, ClassNode.EMPTY_ARRAY, body);
+ }
+
+ private String suffixForField(FieldNode fNode) {
+ // use primitives for efficiently
+ if (fNode.getType() == ClassHelper.int_TYPE) return "Int";
+ if (fNode.getType() == ClassHelper.boolean_TYPE) return "Boolean";
+// currently char isn't found due to a bug, so go with Object
+// if (fNode.getType() == ClassHelper.char_TYPE) return "Char";
+ if (fNode.getType() == ClassHelper.long_TYPE) return "Long";
+ if (fNode.getType() == ClassHelper.short_TYPE) return "Short";
+ if (fNode.getType() == ClassHelper.byte_TYPE) return "Byte";
+ if (fNode.getType() == ClassHelper.float_TYPE) return "Float";
+ if (fNode.getType() == ClassHelper.double_TYPE) return "Double";
+ return "Object";
+ }
+}
Index: src/main/org/codehaus/groovy/transform/CanonicalASTTransformation.java
===================================================================
--- src/main/org/codehaus/groovy/transform/CanonicalASTTransformation.java (revision )
+++ src/main/org/codehaus/groovy/transform/CanonicalASTTransformation.java (revision )
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2008-2010 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.transform.Canonical;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotatedNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.expr.ConstantExpression;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+
+import java.util.ArrayList;
+
+import static org.codehaus.groovy.transform.EqualsAndHashCodeASTTransformation.createEquals;
+import static org.codehaus.groovy.transform.EqualsAndHashCodeASTTransformation.createHashCode;
+import static org.codehaus.groovy.transform.ToStringASTTransformation.createToString;
+import static org.codehaus.groovy.transform.ToStringASTTransformation.toStringInit;
+import static org.codehaus.groovy.transform.TupleConstructorASTTransformation.createConstructor;
+
+/**
+ * Handles generation of code for the @Canonical annotation.
+ *
+ * @author Paulo Poiati
+ * @author Paul King
+ */
+@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
+public class CanonicalASTTransformation extends AbstractASTTransformation {
+
+ static final Class MY_CLASS = Canonical.class;
+ static final ClassNode MY_TYPE = new ClassNode(MY_CLASS);
+ static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
+
+ public void visit(ASTNode[] nodes, SourceUnit source) {
+ init(nodes, source);
+ AnnotatedNode parent = (AnnotatedNode) nodes[1];
+ AnnotationNode node = (AnnotationNode) nodes[0];
+ if (!MY_TYPE.equals(node.getClassNode())) return;
+
+ if (parent instanceof ClassNode) {
+ ClassNode cNode = (ClassNode) parent;
+ // TODO remove - let other validation steps pick this up
+ if (hasAnnotation(cNode, ImmutableASTTransformation.MY_TYPE)) {
+ addError(MY_TYPE_NAME + " class '" + cNode.getName() + "' can't also be " + ImmutableASTTransformation.MY_TYPE_NAME, parent);
+ }
+ checkNotInterface(cNode, MY_TYPE_NAME);
+ createConstructor(cNode, false, false, new ArrayList());
+ createHashCode(cNode, false, false, false, new ArrayList());
+ createEquals(cNode, false, false, new ArrayList());
+ toStringInit(cNode, ConstantExpression.FALSE);
+ createToString(cNode, false, false, new ArrayList());
+ }
+ }
+
+}
Index: src/main/groovy/transform/EqualsAndHashCode.java
===================================================================
--- src/main/groovy/transform/EqualsAndHashCode.java (revision )
+++ src/main/groovy/transform/EqualsAndHashCode.java (revision )
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2008-2010 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 groovy.transform;
+
+import org.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Class annotation used to assist in creating appropriate {@code equals()} and {@code hashCode()} methods.
+ *
+ * It allows you to write classes in this shortened form:
+ *
+ * import groovy.transform.EqualsAndHashCode
+ * {@code @EqualsAndHashCode}
+ * class Person {
+ * String first, last
+ * int age
+ * }
+ * def p1 = new Person(first:'John', last:'Smith', age:21)
+ * def p2 = new Person(first:'John', last:'Smith', age:21)
+ * assert p1 == p2
+ * def map = [:]
+ * map[p1] = 45
+ * assert map[p2] == 45
+ *
+ * The {@code @EqualsAndHashCode} annotation instructs the compiler to execute an
+ * AST transformation which adds the necessary equals and hashCode methods to the class.
+ *
+ * The {@code hashCode()} method is calculated using Groovy's {@code HashCodeHelper} class
+ * which implements an algorithm similar to the outlined in the book Effective Java.
+ *
+ * The {@code equals()} method compares the values of the individual properties of the class.
+ *
+ * @see org.codehaus.groovy.util.HashCodeHelper
+ * @author Paul King
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.EqualsAndHashCodeASTTransformation")
+public @interface EqualsAndHashCode {
+ /**
+ * Comma separated list of field and property names to exclude from equals and hashCode calculations
+ */
+ String excludes() default "";
+
+ /**
+ * Whether to include super in equals and hashCode calculations
+ */
+ boolean callSuper() default false;
+
+ /**
+ * Include fields as well as properties in equals and hashCode calculations
+ */
+ boolean includeFields() default false;
+}
\ No newline at end of file
Index: src/main/groovy/lang/Immutable.java
===================================================================
--- src/main/groovy/lang/Immutable.java (revision 19660)
+++ src/main/groovy/lang/Immutable.java (revision )
@@ -24,6 +24,8 @@
import java.lang.annotation.Target;
/**
+ * Note: This class is Deprecated, please use groovy.transform.Immutable.
+ *
* Class annotation used to assist in the creation of immutable classes.
*
* It allows you to write classes in this shortened form:
@@ -39,7 +41,7 @@
* def c2 = new Customer('Tom', 'Jones', 21, d, ['Books', 'Games'])
* assert c1 == c2
*
- * The @Immutable annotation instructs the compiler to execute an
+ * The {@code @Immutable} annotation instructs the compiler to execute an
* AST transformation which adds the necessary getters, constructors,
* equals, hashCode and other helper methods that are typically written
* when creating immutable classes with the defined properties.
@@ -49,7 +51,7 @@
* The class is automatically made final.
* Properties must be of an immutable type or a type with a strategy for handling non-immutable
* characteristics. Specifically, the type must be one of the primitive or wrapper types, Strings, enums,
- * other @Immutable classes or known immutables (e.g. java.awt.Color, java.net.URI). Also handled are
+ * other {@code @Immutable} classes or known immutables (e.g. java.awt.Color, java.net.URI). Also handled are
* Cloneable classes, collections, maps and arrays, and other "effectively immutable" classes with
* special handling (e.g. java.util.Date).
* Properties automatically have private, final backing fields with getters.
@@ -113,9 +115,11 @@
*
*
* @author Paul King
+ * @deprecated use groovy.transform.Immutable
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@GroovyASTTransformationClass("org.codehaus.groovy.transform.ImmutableASTTransformation")
+@Deprecated
public @interface Immutable {
}
Index: src/main/groovy/transform/Immutable.java
===================================================================
--- src/main/groovy/transform/Immutable.java (revision )
+++ src/main/groovy/transform/Immutable.java (revision )
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2008-2010 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 groovy.transform;
+
+import org.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Class annotation used to assist in the creation of immutable classes.
+ *
+ * It allows you to write classes in this shortened form:
+ *
+ * {@code @Immutable} class Customer {
+ * String first, last
+ * int age
+ * Date since
+ * Collection favItems
+ * }
+ * def d = new Date()
+ * def c1 = new Customer(first:'Tom', last:'Jones', age:21, since:d, favItems:['Books', 'Games'])
+ * def c2 = new Customer('Tom', 'Jones', 21, d, ['Books', 'Games'])
+ * assert c1 == c2
+ *
+ * The {@code @Immutable} annotation instructs the compiler to execute an
+ * AST transformation which adds the necessary getters, constructors,
+ * equals, hashCode and other helper methods that are typically written
+ * when creating immutable classes with the defined properties.
+ *
+ * A class created in this way has the following characteristics:
+ *
+ * - The class is automatically made final.
+ *
- Properties must be of an immutable type or a type with a strategy for handling non-immutable
+ * characteristics. Specifically, the type must be one of the primitive or wrapper types, Strings, enums,
+ * other {@code @Immutable} classes or known immutables (e.g. java.awt.Color, java.net.URI). Also handled are
+ * Cloneable classes, collections, maps and arrays, and other "effectively immutable" classes with
+ * special handling (e.g. java.util.Date).
+ *
- Properties automatically have private, final backing fields with getters.
+ * Attempts to update the property will result in a {@code ReadOnlyPropertyException}.
+ *
- A map-based constructor is provided which allows you to set properties by name.
+ *
- A tuple-style constructor is provided which allows you to set properties in the same order as they are defined.
+ *
- Default {@code equals}, {@code hashCode} and {@code toString} methods are provided based on the property values.
+ * Though not normally required, you may write your own implementations of these methods. For {@code equals} and {@code hashCode},
+ * if you do write your own method, it is up to you to obey the general contract for {@code equals} methods and supply
+ * a corresponding matching {@code hashCode} method.
+ * If you do provide one of these methods explicitly, the default implementation will be made available in a private
+ * "underscore" variant which you can call. E.g., you could provide a (not very elegant) multi-line formatted
+ * {@code toString} method for {@code Customer} above as follows:
+ *
+ * String toString() {
+ * _toString().replaceAll(/\(/, '(\n\t').replaceAll(/\)/, '\n)').replaceAll(/, /, '\n\t')
+ * }
+ *
+ * If an "underscore" version of the respective method already exists, then no default implementation is provided.
+ * - {@code Date}s, {@code Cloneable}s and arrays are defensively copied on the way in (constructor) and out (getters).
+ * Arrays and {@code Cloneable} objects use the {@code clone} method. For your own classes,
+ * it is up to you to define this method and use deep cloning if appropriate.
+ *
- {@code Collection}s and {@code Map}s are wrapped by immutable wrapper classes (but not deeply cloned!).
+ * Attempts to update them will result in an {@code UnsupportedOperationException}.
+ *
- Fields that are enums or other {@code @Immutable} classes are allowed but for an
+ * otherwise possible mutable property type, an error is thrown.
+ *
- You don't have to follow Groovy's normal property conventions, e.g. you can create an explicit private field and
+ * then you can write explicit get and set methods. Such an approach, isn't currently prohibited (to give you some
+ * wiggle room to get around these conventions) but any fields created in this way are deemed not to be part of the
+ * significant state of the object and aren't factored into the {@code equals} or {@code hashCode} methods.
+ * Similarly, you may use static properties (though usually this is discouraged) and these too will be ignored
+ * as far as significant state is concerned. If you do break standard conventions, you do so at your own risk and
+ * your objects may no longer be immutable. It is up to you to ensure that your objects remain immutable at least
+ * to the extent expected in other parts of your program!
+ *
+ *
+ * Immutable classes are particularly useful for functional and concurrent styles of programming
+ * and for use as key values within maps.
+ *
+ * Limitations:
+ *
+ * -
+ * As outlined above, Arrays and {@code Cloneable} objects use the {@code clone} method. For your own classes,
+ * it is up to you to define this method and use deep cloning if appropriate.
+ *
+ * -
+ * As outlined above, {@code Collection}s and {@code Map}s are wrapped by immutable wrapper classes (but not deeply cloned!).
+ *
+ * -
+ * Currently {@code BigInteger} and {#code BigDecimal} are deemed immutable but see:
+ * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6348370
+ *
+ * -
+ * {@code java.awt.Color} is treated as "effectively immutable" but is not final so while not normally used with child
+ * classes, it isn't strictly immutable. Use at your own risk.
+ *
+ * -
+ * {@code java.util.Date} is treated as "effectively immutable" but is not final so it isn't strictly immutable.
+ * Use at your own risk.
+ *
+ *
+ *
+ * @author Paul King
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.ImmutableASTTransformation")
+public @interface Immutable {
+}
Index: src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy
===================================================================
--- src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy (revision 20003)
+++ src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy (revision )
@@ -1,5 +1,5 @@
/*
- * Copyright 2008-2009 the original author or authors.
+ * Copyright 2008-2010 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.
@@ -22,31 +22,47 @@
void testImmutable() {
def objects = evaluate("""
+ import groovy.transform.Immutable
- enum Coin { HEAD, TAIL }
- @Immutable class Bar {
- String x, y
- Coin c
- Collection nums
- }
- [new Bar(x:'x', y:'y', c:Coin.HEAD, nums:[1,2]),
- new Bar('x', 'y', Coin.HEAD, [1,2])]
+ enum Coin { HEAD, TAIL }
+ @Immutable class Bar {
+ String x, y
+ Coin c
+ Collection nums
+ }
+ [new Bar(x:'x', y:'y', c:Coin.HEAD, nums:[1,2]),
+ new Bar('x', 'y', Coin.HEAD, [1,2])]
""")
assertEquals objects[0].hashCode(), objects[1].hashCode()
assertEquals objects[0], objects[1]
assertTrue objects[0].nums.class.name.contains("Unmodifiable")
}
-
+
+ void testImmutableCantAlsoBeMutable() {
+ def msg = shouldFail(RuntimeException) {
+ assertScript """
+ import groovy.transform.*
+ @Immutable
+ @Canonical
+ class Foo {
+ String bar
+ }
+ """
+ }
+ assert msg.contains("@Canonical class 'Foo' can't also be @Immutable")
+ }
+
void testImmutableListProp() {
def objects = evaluate("""
+ import groovy.transform.Immutable
- @Immutable class HasList {
- String[] letters
- List nums
- }
- def letters = 'A,B,C'.split(',')
- def nums = [1, 2]
- [new HasList(letters:letters, nums:nums),
- new HasList(letters, nums)]
+ @Immutable class HasList {
+ String[] letters
+ List nums
+ }
+ def letters = 'A,B,C'.split(',')
+ def nums = [1, 2]
+ [new HasList(letters:letters, nums:nums),
+ new HasList(letters, nums)]
""")
assertEquals objects[0].hashCode(), objects[1].hashCode()
@@ -57,6 +73,7 @@
void testImmutableAsMapKey() {
assertScript """
+ import groovy.transform.Immutable
@Immutable final class HasString {
String s
}
@@ -69,6 +86,7 @@
void testImmutableWithOnlyMap() {
assertScript """
+ import groovy.transform.Immutable
@Immutable final class HasMap {
Map map
}
@@ -79,6 +97,7 @@
void testImmutableWithInvalidPropertyName() {
def msg = shouldFail(MissingPropertyException) {
assertScript """
+ import groovy.transform.Immutable
@Immutable class Simple { }
new Simple(missing:'Name')
"""
@@ -88,6 +107,7 @@
void testImmutableWithHashMap() {
assertScript """
+ import groovy.transform.Immutable
@Immutable final class HasHashMap {
HashMap map = [d:4]
}
@@ -105,6 +125,7 @@
void testImmutableEquals() {
assertScript """
+ import groovy.transform.Immutable
@Immutable class This { String value }
@Immutable class That { String value }
class Other { }
@@ -121,6 +142,7 @@
void testExistingToString() {
assertScript """
+ import groovy.transform.Immutable
@Immutable class Foo {
String value
}
@@ -143,6 +165,7 @@
void testExistingEquals() {
assertScript """
+ import groovy.transform.Immutable
@Immutable class Foo {
String value
}
@@ -184,6 +207,7 @@
void testExistingHashCode() {
assertScript """
+ import groovy.transform.Immutable
@Immutable class Foo {
String value
}
@@ -218,6 +242,7 @@
void testStaticsAllowed_ThoughUsuallyBadDesign() {
// design here is questionable as getDescription() method is not idempotent
assertScript '''
+ import groovy.transform.Immutable
@Immutable class Person {
String first, last
static species = 'Human'
@@ -243,4 +268,4 @@
assert spock.description == 'Leonard Nimoy is a Vulcan'
'''
}
-}
\ No newline at end of file
+}
Index: src/main/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
===================================================================
--- src/main/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java (revision )
+++ src/main/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java (revision )
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2008-2010 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.transform.TupleConstructor;
+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.expr.ConstantExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.FieldExpression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.runtime.DefaultGroovyMethods;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.codehaus.groovy.transform.AbstractASTTransformUtil.assignStatement;
+import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstanceNonPropertyFields;
+import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstancePropertyFields;
+
+/**
+ * Handles generation of code for the @TupleConstructor annotation.
+ *
+ * @author Paul King
+ */
+@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
+public class TupleConstructorASTTransformation extends AbstractASTTransformation {
+
+ static final Class MY_CLASS = TupleConstructor.class;
+ static final ClassNode MY_TYPE = new ClassNode(MY_CLASS);
+ static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
+ private static Map, Expression> primitivesInitialValues;
+
+ static {
+ final ConstantExpression zero = new ConstantExpression(0);
+ final ConstantExpression zeroDecimal = new ConstantExpression(.0);
+ primitivesInitialValues = new HashMap, Expression>();
+ primitivesInitialValues.put(int.class, zero);
+ primitivesInitialValues.put(long.class, zero);
+ primitivesInitialValues.put(short.class, zero);
+ primitivesInitialValues.put(byte.class, zero);
+ primitivesInitialValues.put(char.class, zero);
+ primitivesInitialValues.put(float.class, zeroDecimal);
+ primitivesInitialValues.put(double.class, zeroDecimal);
+ primitivesInitialValues.put(boolean.class, ConstantExpression.FALSE);
+ }
+
+ 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;
+ checkNotInterface(cNode, MY_TYPE_NAME);
+ boolean includeFields = memberHasValue(anno, "includeFields", true);
+ boolean force = memberHasValue(anno, "force", true);
+ String rawExcludes = (String)getMemberValue(anno, "excludes");
+ List excludes = rawExcludes == null ? new ArrayList() : DefaultGroovyMethods.tokenize(rawExcludes, ", ");
+ createConstructor(cNode, includeFields, force, excludes);
+ }
+ }
+
+ public static void createConstructor(ClassNode cNode, boolean includeFields, boolean force, List excludes) {
+ // no processing if existing constructors found
+ if (cNode.getDeclaredConstructors().size() != 0 && !force) return;
+
+ List list = getInstancePropertyFields(cNode);
+ if (includeFields) {
+ list.addAll(getInstanceNonPropertyFields(cNode));
+ }
+ final List params = new ArrayList();
+ final BlockStatement body = new BlockStatement();
+ for (FieldNode fNode : list) {
+ if (excludes.contains(fNode.getName()) || fNode.getName().contains("$")) continue;
+ Parameter param = new Parameter(fNode.getType(), fNode.getName());
+ param.setInitialExpression(providedOrDefaultInitialValue(fNode));
+ params.add(param);
+ body.addStatement(assignStatement(new FieldExpression(fNode), new VariableExpression(param.getName())));
+ }
+ cNode.addConstructor(new ConstructorNode(ACC_PUBLIC, params.toArray(new Parameter[params.size()]), ClassNode.EMPTY_ARRAY, body));
+ }
+
+ private static Expression providedOrDefaultInitialValue(FieldNode fNode) {
+ Expression initialExp = fNode.getInitialExpression() != null ? fNode.getInitialExpression() : ConstantExpression.NULL;
+ final ClassNode paramType = fNode.getType();
+ if (ClassHelper.isPrimitiveType(paramType) && initialExp.equals(ConstantExpression.NULL)) {
+ initialExp = primitivesInitialValues.get(paramType.getTypeClass());
+ }
+ return initialExp;
+ }
+
+}
Index: src/main/org/codehaus/groovy/transform/ImmutableASTTransformation.java
===================================================================
--- src/main/org/codehaus/groovy/transform/ImmutableASTTransformation.java (revision 20420)
+++ src/main/org/codehaus/groovy/transform/ImmutableASTTransformation.java (revision )
@@ -15,30 +15,47 @@
*/
package org.codehaus.groovy.transform;
-import groovy.lang.Immutable;
import groovy.lang.MetaClass;
import groovy.lang.MissingPropertyException;
-import org.codehaus.groovy.ast.*;
+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.PropertyNode;
import org.codehaus.groovy.ast.expr.*;
-import org.codehaus.groovy.ast.stmt.*;
+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.Statement;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.InvokerHelper;
-import org.codehaus.groovy.syntax.Token;
-import org.codehaus.groovy.syntax.Types;
-import org.codehaus.groovy.util.HashCodeHelper;
-import org.objectweb.asm.Opcodes;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import static org.codehaus.groovy.transform.AbstractASTTransformUtil.*;
+import static org.codehaus.groovy.transform.EqualsAndHashCodeASTTransformation.createEquals;
+import static org.codehaus.groovy.transform.EqualsAndHashCodeASTTransformation.createHashCode;
+import static org.codehaus.groovy.transform.ToStringASTTransformation.createToString;
+
/**
* Handles generation of code for the @Immutable annotation.
*
* @author Paul King
*/
@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
-public class ImmutableASTTransformation implements ASTTransformation, Opcodes {
+public class ImmutableASTTransformation extends AbstractASTTransformation {
/*
Currently leaving BigInteger and BigDecimal in list but see:
@@ -62,49 +79,32 @@
"java.awt.Color",
"java.net.URI"
);
- 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 Class MY_CLASS = groovy.transform.Immutable.class;
+ static final ClassNode MY_TYPE = new ClassNode(MY_CLASS);
+ static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
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 INVOKER_TYPE = new ClassNode(InvokerHelper.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 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));
- }
-
+ init(nodes, source);
AnnotatedNode parent = (AnnotatedNode) nodes[1];
AnnotationNode node = (AnnotationNode) nodes[0];
if (!MY_TYPE.equals(node.getClassNode())) return;
- List newNodes = new ArrayList();
+ List newProperties = new ArrayList();
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);
- }
+ checkNotInterface(cNode, MY_TYPE_NAME);
+ makeClassFinal(cNode);
final List pList = getInstanceProperties(cNode);
for (PropertyNode pNode : pList) {
- adjustPropertyForImmutability(pNode, newNodes);
+ adjustPropertyForImmutability(pNode, newProperties);
}
- for (PropertyNode pNode : newNodes) {
+ for (PropertyNode pNode : newProperties) {
cNode.getProperties().remove(pNode);
addProperty(cNode, pNode);
}
@@ -112,199 +112,24 @@
for (FieldNode fNode : fList) {
ensureNotPublic(cName, fNode);
}
- createConstructor(cNode);
- createHashCode(cNode);
- createEquals(cNode);
- createToString(cNode);
+ createConstructors(cNode);
+ createHashCode(cNode, true, false, false, new ArrayList());
+ createEquals(cNode, false, false, new ArrayList());
+ createToString(cNode, false, false, new ArrayList());
}
}
- private boolean hasDeclaredMethod(ClassNode cNode, String name, int argsCount) {
- List ms = cNode.getDeclaredMethods(name);
- for(MethodNode m : ms) {
- Parameter[] paras = m.getParameters();
- if(paras != null && paras.length == argsCount) {
- return true;
+ private void makeClassFinal(ClassNode cNode) {
+ if ((cNode.getModifiers() & ACC_FINAL) == 0) {
+ cNode.setModifiers(cNode.getModifiers() | ACC_FINAL);
- }
- }
+ }
+ }
- 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 list = getInstanceProperties(cNode);
-
- 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 list = getInstanceProperties(cNode);
- // 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 StaticMethodCallExpression(INVOKER_TYPE, "toString", fieldExpr)));
- }
- 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 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));
-
- body.addStatement(new ExpressionStatement(new BinaryExpression(other, ASSIGN, new CastExpression(cNode, other))));
-
- final List list = getInstanceProperties(cNode);
- // 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) {
+ private void createConstructors(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 FieldNode constructorField = cNode.addField("$print$names", 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());
- }
+ if (!validateConstructors(cNode)) return;
List list = getInstanceProperties(cNode);
boolean specialHashMapCase = list.size() == 1 && list.get(0).getField().getType().equals(HASHMAP_TYPE);
@@ -316,16 +141,36 @@
}
}
- private List getInstanceProperties(ClassNode cNode) {
- final List result = new ArrayList();
- for (PropertyNode pNode : cNode.getProperties()) {
- if (!pNode.isStatic()) {
- result.add(pNode);
+ private void createConstructorOrdered(ClassNode cNode, FieldExpression constructorStyle, List list) {
+ final MapExpression argMap = new MapExpression();
+ final Parameter[] orderedParams = new Parameter[list.size()];
+ int index = 0;
+ for (PropertyNode pNode : list) {
+ Parameter param = new Parameter(pNode.getField().getType(), pNode.getField().getName());
+ orderedParams[index++] = param;
+ 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));
- }
+ }
- return result;
+
+ private Statement createGetterBodyDefault(FieldNode fNode) {
+ final Expression fieldExpr = new FieldExpression(fNode);
+ return new ExpressionStatement(fieldExpr);
}
+ private Expression cloneCollectionExpr(Expression fieldExpr) {
+ return new StaticMethodCallExpression(DGM_TYPE, "asImmutable", fieldExpr);
+ }
+
+ private Expression cloneArrayOrCloneableExpr(Expression fieldExpr) {
+ return new MethodCallExpression(fieldExpr, "clone", MethodCallExpression.NO_ARGUMENTS);
+ }
+
private void createConstructorMapSpecial(ClassNode cNode, FieldExpression constructorStyle, List list) {
final BlockStatement body = new BlockStatement();
body.addStatement(createConstructorStatementMapSpecial(list.get(0).getField()));
@@ -358,26 +203,62 @@
body)));
}
- private void createConstructorOrdered(ClassNode cNode, FieldExpression constructorStyle, List 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()));
+ 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)))
+ )
+ );
- }
+ }
- 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 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("$")) {
+ addError("Public field '" + fName + "' not allowed for " + MY_TYPE_NAME + " class '" + cNode + "'.", fNode);
- }
+ }
+ }
+ 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 boolean validateConstructors(ClassNode cNode) {
+ if (cNode.getDeclaredConstructors().size() != 0) {
+ // TODO: allow constructors which only call provided constructor?
+ addError("Explicit constructors not allowed for " + ImmutableASTTransformation.MY_TYPE_NAME + " class: " + cNode.getNameWithoutPackage(), cNode.getDeclaredConstructors().get(0));
+ }
+ return true;
+ }
+
private Statement createConstructorStatement(ClassNode cNode, PropertyNode pNode) {
FieldNode fNode = pNode.getField();
final ClassNode fieldType = fNode.getType();
- final Statement statement;
+ Statement statement = null;
if (fieldType.isArray() || fieldType.implementsInterface(CLONEABLE_TYPE)) {
statement = createConstructorStatementArrayOrCloneable(fNode);
} else if (fieldType.isDerivedFrom(DATE_TYPE)) {
@@ -387,17 +268,13 @@
} else if (isKnownImmutable(fieldType)) {
statement = createConstructorStatementDefault(fNode);
} else if (fieldType.isResolved()) {
- throw new RuntimeException(createErrorMessage(cNode.getName(), fNode.getName(), fieldType.getName(), "compiling"));
+ addError(createErrorMessage(cNode.getName(), fNode.getName(), fieldType.getName(), "compiling"), fNode);
} else {
statement = createConstructorStatementGuarded(cNode, fNode);
}
return statement;
}
- private boolean isOrImplements(ClassNode fieldType, ClassNode interfaceType) {
- return fieldType.equals(interfaceType) || fieldType.implementsInterface(interfaceType);
- }
-
private Statement createConstructorStatementGuarded(ClassNode cNode, FieldNode fNode) {
final FieldExpression fieldExpr = new FieldExpression(fNode);
Expression initExpr = fNode.getInitialValueExpression();
@@ -431,32 +308,6 @@
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;
return fieldType.isEnum() ||
@@ -468,20 +319,6 @@
return immutableList.contains(typeName);
}
- 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();
@@ -515,49 +352,6 @@
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 MethodCallExpression(self, "is", new ArgumentListExpression(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 newNodes) {
final FieldNode fNode = pNode.getField();
fNode.setModifiers((pNode.getModifiers() & (~ACC_PUBLIC)) | ACC_FINAL | ACC_PRIVATE);
@@ -585,16 +379,10 @@
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" +
+ MY_TYPE_NAME + " classes only support properties with effectively immutable types including:\n" +
"- Strings, primitive types, wrapper types, BigInteger and BigDecimal, enums\n" +
"- other " + MY_TYPE_NAME + " classes and known immutables (java.awt.Color, java.net.URI)\n" +
"- Cloneable classes, collections, maps and arrays, and other classes with special handling (java.util.Date)\n" +
@@ -611,27 +399,12 @@
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));
- }
-
public static Object checkImmutable(String className, String fieldName, Object field) {
if (field == null || field instanceof Enum || inImmutableList(field.getClass().getName())) return field;
if (field instanceof Collection) return DefaultGroovyMethods.asImmutable((Collection) field);
Index: src/main/org/codehaus/groovy/transform/AbstractASTTransformUtil.java
===================================================================
--- src/main/org/codehaus/groovy/transform/AbstractASTTransformUtil.java (revision )
+++ src/main/org/codehaus/groovy/transform/AbstractASTTransformUtil.java (revision )
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2008-2010 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 org.codehaus.groovy.ast.ClassNode;
+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.*;
+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.syntax.Token;
+import org.codehaus.groovy.syntax.Types;
+import org.objectweb.asm.Opcodes;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class AbstractASTTransformUtil implements Opcodes {
+ 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 INSTANCEOF = Token.newSymbol(Types.KEYWORD_INSTANCEOF, -1, -1);
+ private static final Token ASSIGN = Token.newSymbol(Types.ASSIGN, -1, -1);
+
+ public static boolean hasDeclaredMethod(ClassNode cNode, String name, int argsCount) {
+ List ms = cNode.getDeclaredMethods(name);
+ for (MethodNode m : ms) {
+ Parameter[] paras = m.getParameters();
+ if (paras != null && paras.length == argsCount) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static Statement returnFalseIfWrongType(ClassNode cNode, Expression other) {
+ return new IfStatement(
+ notEqualClasses(cNode, other),
+ new ReturnStatement(ConstantExpression.FALSE),
+ new EmptyStatement()
+ );
+ }
+
+ public static IfStatement returnFalseIfNull(Expression other) {
+ return new IfStatement(
+ equalsNullExpr(other),
+ new ReturnStatement(ConstantExpression.FALSE),
+ new EmptyStatement()
+ );
+ }
+
+ public static IfStatement returnTrueIfIdentical(Expression self, Expression other) {
+ return new IfStatement(
+ identicalExpr(self, other),
+ new ReturnStatement(ConstantExpression.TRUE),
+ new EmptyStatement()
+ );
+ }
+
+ public static Statement returnFalseIfPropertyNotEqual(FieldNode fNode, Expression other) {
+ return new IfStatement(
+ notEqualsExpr(fNode, other),
+ new ReturnStatement(ConstantExpression.FALSE),
+ new EmptyStatement()
+ );
+ }
+
+ public static List getInstanceProperties(ClassNode cNode) {
+ final List result = new ArrayList();
+ for (PropertyNode pNode : cNode.getProperties()) {
+ if (!pNode.isStatic()) {
+ result.add(pNode);
+ }
+ }
+ return result;
+ }
+
+ public static List getInstancePropertyFields(ClassNode cNode) {
+ final List result = new ArrayList();
+ for (PropertyNode pNode : cNode.getProperties()) {
+ if (!pNode.isStatic()) {
+ result.add(pNode.getField());
+ }
+ }
+ return result;
+ }
+
+ public static List getInstanceNonPropertyFields(ClassNode cNode) {
+ final List result = new ArrayList();
+ for (FieldNode fNode : cNode.getFields()) {
+ if (!fNode.isStatic() && cNode.getProperty(fNode.getName()) == null) {
+ result.add(fNode);
+ }
+ }
+ return result;
+ }
+
+ public static List getInstanceFields(ClassNode cNode) {
+ final List result = new ArrayList();
+ for (FieldNode fNode : cNode.getFields()) {
+ if (!fNode.isStatic()) {
+ result.add(fNode);
+ }
+ }
+ return result;
+ }
+
+ public static Statement assignStatement(Expression fieldExpr, Expression value) {
+ return new ExpressionStatement(assignExpr(fieldExpr, value));
+ }
+
+ private static Expression assignExpr(Expression expression, Expression value) {
+ return new BinaryExpression(expression, ASSIGN, value);
+ }
+
+ public static BooleanExpression isInstanceOf(Expression objectExpression, ClassNode cNode) {
+ return new BooleanExpression(new BinaryExpression(objectExpression, INSTANCEOF, new ClassExpression(cNode)));
+ }
+
+ public static BooleanExpression equalsNullExpr(Expression argExpr) {
+ return new BooleanExpression(new BinaryExpression(argExpr, COMPARE_EQUAL, ConstantExpression.NULL));
+ }
+
+ public static BooleanExpression isZeroExpr(Expression expr) {
+ return new BooleanExpression(new BinaryExpression(expr, COMPARE_EQUAL, new ConstantExpression(0)));
+ }
+
+ private static BooleanExpression notEqualsExpr(FieldNode fNode, Expression other) {
+ final Expression fieldExpr = new FieldExpression(fNode);
+ final Expression otherExpr = new PropertyExpression(other, fNode.getName());
+ return new BooleanExpression(new BinaryExpression(fieldExpr, COMPARE_NOT_EQUAL, otherExpr));
+ }
+
+ private static BooleanExpression identicalExpr(Expression self, Expression other) {
+ return new BooleanExpression(new MethodCallExpression(self, "is", new ArgumentListExpression(other)));
+ }
+
+ private static BooleanExpression notEqualClasses(ClassNode cNode, Expression other) {
+ return new BooleanExpression(new BinaryExpression(new ClassExpression(cNode), COMPARE_NOT_EQUAL,
+ new MethodCallExpression(other, "getClass", MethodCallExpression.NO_ARGUMENTS)));
+ }
+
+ public static boolean isOrImplements(ClassNode fieldType, ClassNode interfaceType) {
+ return fieldType.equals(interfaceType) || fieldType.implementsInterface(interfaceType);
+ }
+
+ public static BooleanExpression isTrueExpr(Expression argExpr) {
+ return new BooleanExpression(new BinaryExpression(argExpr, COMPARE_EQUAL, ConstantExpression.TRUE));
+ }
+
+ public static BooleanExpression isOneExpr(Expression expr) {
+ return new BooleanExpression(new BinaryExpression(expr, COMPARE_EQUAL, new ConstantExpression(1)));
+ }
+
+ public static Statement safeExpression(Expression fieldExpr, Expression expression) {
+ return new IfStatement(
+ equalsNullExpr(fieldExpr),
+ new ExpressionStatement(fieldExpr),
+ new ExpressionStatement(expression));
+ }
+
+ public static 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));
+ }
+
+ public static Expression findArg(String fName) {
+ return new PropertyExpression(new VariableExpression("args"), fName);
+ }
+
+}
Index: src/main/groovy/transform/AutoClone.java
===================================================================
--- src/main/groovy/transform/AutoClone.java (revision )
+++ src/main/groovy/transform/AutoClone.java (revision )
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2008-2010 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 groovy.transform;
+
+import org.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Note: This annotation is currently experimental! Use at your own risk!
+ *
+ * Class annotation used to assist in the creation of {@code Cloneable} classes.
+ * The {@code @AutoClone} annotation instructs the compiler to execute an
+ * AST transformation which adds a public {@code clone()} method and adds
+ * {@code Cloneable} to the interfaces which the class implements. The {@code clone()}
+ * method will call {@code super.clone()} before calling {@code clone()} on each
+ * {@code Cloneable} property of the class.
+ *
+ * Example usage:
+ *
+ * import groovy.transform.AutoClone
+ * {@code @AutoClone}
+ * class Person {
+ * String first, last
+ * List favItems
+ * Date since
+ * }
+ *
+ * Which will create a class of the following form:
+ *
+ * class Person implements Cloneable {
+ * ...
+ * public Object clone() throws CloneNotSupportedException {
+ * Object result = super.clone()
+ * result.favItems = favItems.clone()
+ * result.since = since.clone()
+ * return result
+ * }
+ * ...
+ * }
+ *
+ * Which can be used as follows:
+ *
+ * def p = new Person(first:'John', last:'Smith', favItems:['ipod', 'shiraz'], since:new Date())
+ * def p2 = p.clone()
+ *
+ * assert p instanceof Cloneable
+ * assert p.favItems instanceof Cloneable
+ * assert p.since instanceof Cloneable
+ * assert !(p.first instanceof Cloneable)
+ *
+ * assert !p.is(p2)
+ * assert !p.favItems.is(p2.favItems)
+ * assert !p.since.is(p2.since)
+ * assert p.first.is(p2.first)
+ *
+ * In the above example, {@code super.clone()} is called which in this case
+ * calls {@code clone()} from {@code java.lang.Object}. This does a bit-wise
+ * copy of all the properties (references and primitive values). Properties
+ * like {@code first} has type {@code String} which is not {@code Cloneable}
+ * so it is left as the bit-wise copy. Both {@code Date} and {@code ArrayList}
+ * are {@code Cloneable} so the {@code clone()} method on each of those properties
+ * will be called. For the list, a shallow copy is made during its {@code clone()} method.
+ *
+ * If your classes require deep cloning, it is up to you to provide the appropriate
+ * deep cloning logic in the respective {@code clone()} method for your class.
+ *
+ * If one of your properties contains an object that doesn't support cloning
+ * or attempts deep copying of a data structure containing an object that
+ * doesn't support cloning, then a {@code CloneNotSupportedException} may occur
+ * at runtime.
+ *
+ * If any of your fields are {@code final} and {@code Cloneable} you should set
+ * {@code useCopyConstructor=true} which will then use the copy constructor pattern.
+ * Here is an example making use of the copy constructor pattern:
+ *
+ * import groovy.transform.AutoClone
+ * import static groovy.transform.AutoCloneStyle.*
+ * {@code @AutoClone(style=COPY_CONSTRUCTOR)}
+ * class Person {
+ * final String first, last
+ * final Date birthday
+ * }
+ * {@code @AutoClone(style=COPY_CONSTRUCTOR)}
+ * class Customer extends Person {
+ * final int numPurchases
+ * final List favItems
+ * }
+ *
+ * Which will create classes of the following form:
+ *
+ * class Person implements Cloneable {
+ * ...
+ * protected Person(Person other) throws CloneNotSupportedException {
+ * super.clone()
+ * birthday = other.birthday.clone()
+ * }
+ * public Object clone() throws CloneNotSupportedException {
+ * return new Person(this)
+ * }
+ * ...
+ * }
+ * class Customer extends Person {
+ * ...
+ * protected Customer(Customer other) throws CloneNotSupportedException {
+ * super(other)
+ * numPurchases = other.numPurchases
+ * favItems = other.favItems.clone()
+ * }
+ * public Object clone() throws CloneNotSupportedException {
+ * return new Customer(this)
+ * }
+ * ...
+ * }
+ *
+ * If you use this style on a child class, the parent class must
+ * also have a copy constructor. This approach is slightly slower than
+ * the traditional cloning approach but the {@code Cloneable} fields
+ * of your class can be final.
+ *
+ * As a final example, if your class already implements the {@code Serializable}
+ * or {@code Externalizable} interface, you can choose the following cloning style:
+ *
+ * {@code @AutoClone(style=SERIALIZATION)}
+ * class Person implements Serializable {
+ * String first, last
+ * Date birthday
+ * }
+ *
+ * which outputs a class with the following form:
+ *
+ * class Person implements Cloneable, Serializable {
+ * ...
+ * Object clone() throws CloneNotSupportedException {
+ * def baos = new ByteArrayOutputStream()
+ * baos.withObjectOutputStream{ it.writeObject(this) }
+ * def bais = new ByteArrayInputStream(baos.toByteArray())
+ * bais.withObjectInputStream(getClass().classLoader){ it.readObject() }
+ * }
+ * ...
+ * }
+ *
+ * This will output an error if your class doesn't implement one of
+ * {@code Serializable} or {@code Externalizable}, will typically be
+ * significantly slower than the other approaches, also doesn't
+ * allow fields to be final, will take up more memory as even immutable classes
+ * like String will be cloned but does have the advantage that it performs
+ * deep cloning automatically.
+ *
+ * @author Paul King
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.AutoCloneASTTransformation")
+public @interface AutoClone {
+ /**
+ * Comma separated list of property names to exclude from cloning
+ */
+ String excludes() default "";
+
+ /**
+ * Include fields as well as properties when cloning
+ */
+ boolean includeFields() default false;
+
+ /**
+ * Style to use when cloning
+ */
+ groovy.transform.AutoCloneStyle style() default AutoCloneStyle.CLONE;
+}
Index: src/main/groovy/transform/Canonical.java
===================================================================
--- src/main/groovy/transform/Canonical.java (revision )
+++ src/main/groovy/transform/Canonical.java (revision )
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2008-2010 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 groovy.transform;
+
+import org.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Class annotation used to assist in the creation of mutable classes.
+ *
+ * It allows you to write classes in this shortened form:
+ *
+ * {@code @Canonical} class Customer {
+ * String first, last
+ * int age
+ * Date since
+ * Collection favItems = ['Food']
+ * def object
+ * }
+ * def d = new Date()
+ * def anyObject = new Object()
+ * def c1 = new Customer(first:'Tom', last:'Jones', age:21, since:d, favItems:['Books', 'Games'], object: anyObject)
+ * def c2 = new Customer('Tom', 'Jones', 21, d, ['Books', 'Games'], anyObject)
+ * assert c1 == c2
+ *
+ * If you set the autoDefaults flag to true, you don't need to provide all arguments in constructors calls,
+ * in this case all properties not present are initialized to the default value:
+ * def c3 = new Customer(last: 'Jones', age: 21)
+ * def c4 = new Customer('Tom', 'Jones')
+ *
+ * assert null == c3.since
+ * assert 0 == c4.age
+ * assert c3.favItems == ['Food'] && c4.favItems == ['Food']
+ *
+ *
+ * The {@code @Canonical} annotation instructs the compiler to execute an
+ * AST transformation which adds positional constructors,
+ * equals, hashCode and a pretty print toString.
+ *
+ * A class created in this way has the following characteristics:
+ *
+ * - A no-arg constructor is provided which allows you to set properties by name using Groovy's normal bean conventions.
+ *
- Tuple-style constructors are provided which allow you to set properties in the same order as they are defined.
+ *
- Default {@code equals}, {@code hashCode} and {@code toString} methods are provided based on the property values.
+ * Though not normally required, you may write your own implementations of these methods. For {@code equals} and {@code hashCode},
+ * if you do write your own method, it is up to you to obey the general contract for {@code equals} methods and supply
+ * a corresponding matching {@code hashCode} method.
+ * If you do provide one of these methods explicitly, the default implementation will be made available in a private
+ * "underscore" variant which you can call. E.g., you could provide a (not very elegant) multi-line formatted
+ * {@code toString} method for {@code Customer} above as follows:
+ *
+ * String toString() {
+ * _toString().replaceAll(/\(/, '(\n\t').replaceAll(/\)/, '\n)').replaceAll(/, /, '\n\t')
+ * }
+ *
+ * If an "underscore" version of the respective method already exists, then no default implementation is provided.
+ *
+ *
+ * Limitations:
+ *
+ * -
+ * If you explicitly add your own constructors, then the transformation will not add any other constructor to the class.
+ *
+ *
+ * @author Paulo Poiati
+ * @author Paul King
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.TYPE})
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.CanonicalASTTransformation")
+public @interface Canonical {}
Index: src/test/org/codehaus/groovy/transform/TransformsAndCustomClassLoadersTest.groovy
===================================================================
--- src/test/org/codehaus/groovy/transform/TransformsAndCustomClassLoadersTest.groovy (revision 19553)
+++ src/test/org/codehaus/groovy/transform/TransformsAndCustomClassLoadersTest.groovy (revision )
@@ -39,7 +39,7 @@
def transformLoader = new GroovyClassLoader(TransformsAndCustomClassLoadersTest.classLoader)
checkIsIsolated(resolvingLoader)
- def clazz = compileAndLoadClass("@Immutable class Foo { String bar }", resolvingLoader, transformLoader)
+ def clazz = compileAndLoadClass("@groovy.transform.Immutable class Foo { String bar }", resolvingLoader, transformLoader)
checkIsImmutable(clazz)
}
Index: src/main/org/codehaus/groovy/transform/AutoCloneASTTransformation.java
===================================================================
--- src/main/org/codehaus/groovy/transform/AutoCloneASTTransformation.java (revision )
+++ src/main/org/codehaus/groovy/transform/AutoCloneASTTransformation.java (revision )
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2008-2010 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.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.FieldNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.VariableScope;
+import org.codehaus.groovy.ast.expr.*;
+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 java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstanceNonPropertyFields;
+import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstancePropertyFields;
+import static org.codehaus.groovy.transform.AbstractASTTransformUtil.isInstanceOf;
+
+/**
+ * Handles generation of code for the @AutoClone annotation.
+ *
+ * @author Paul King
+ */
+@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
+public class AutoCloneASTTransformation extends AbstractASTTransformation {
+ static final Class MY_CLASS = AutoClone.class;
+ static final ClassNode MY_TYPE = new ClassNode(MY_CLASS);
+ static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
+ private static final ClassNode CLONEABLE_TYPE = ClassHelper.make(Cloneable.class);
+ private static final ClassNode BAOS_TYPE = ClassHelper.make(ByteArrayOutputStream.class);
+ private static final ClassNode BAIS_TYPE = ClassHelper.make(ByteArrayInputStream.class);
+ private static final Token ASSIGN = Token.newSymbol(Types.ASSIGN, -1, -1);
+
+ 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;
+ checkNotInterface(cNode, MY_TYPE_NAME);
+ cNode.addInterface(CLONEABLE_TYPE);
+ boolean includeFields = memberHasValue(anno, "includeFields", true);
+ AutoCloneStyle style = getStyle(anno, "style");
+ String rawExcludes = (String) getMemberValue(anno, "excludes");
+ List excludes = rawExcludes == null ? new ArrayList() : DefaultGroovyMethods.tokenize(rawExcludes, ", ");
+ List 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, list, excludes); break;
+ case CLONE:
+ createClone(cNode, list, excludes); break;
+ }
+ }
+ }
+
+ private void createCloneSerialization(ClassNode cNode, List list, List excludes) {
+ final BlockStatement body = new BlockStatement();
+ // def baos = new ByteArrayOutputStream()
+ final Expression baos = new VariableExpression("baos");
+ body.addStatement(new ExpressionStatement(new DeclarationExpression(baos, ASSIGN, new ConstructorCallExpression(BAOS_TYPE, MethodCallExpression.NO_ARGUMENTS))));
+
+ // baos.withObjectOutputStream{ it.writeObject(this) }
+ BlockStatement writeClosureCode = new BlockStatement();
+ final Expression it = new VariableExpression("it");
+ writeClosureCode.addStatement(new ExpressionStatement(new MethodCallExpression(it, "writeObject", VariableExpression.THIS_EXPRESSION)));
+ ClosureExpression writeClosure = new ClosureExpression(new Parameter[]{}, writeClosureCode);
+ writeClosure.setVariableScope(new VariableScope());
+ body.addStatement(new ExpressionStatement(new MethodCallExpression(baos, "withObjectOutputStream", new ArgumentListExpression(writeClosure))));
+
+ // def bais = new ByteArrayInputStream(baos.toByteArray())
+ final Expression bais = new VariableExpression("bais");
+ ConstructorCallExpression bytes = new ConstructorCallExpression(BAIS_TYPE, new TupleExpression(new MethodCallExpression(baos, "toByteArray", MethodCallExpression.NO_ARGUMENTS)));
+ body.addStatement(new ExpressionStatement(new DeclarationExpression(bais, ASSIGN, bytes)));
+
+ // return bais.withObjectInputStream(getClass().classLoader){ it.readObject() }
+ BlockStatement readClosureCode = new BlockStatement();
+ readClosureCode.addStatement(new ExpressionStatement(new MethodCallExpression(it, "readObject", MethodCallExpression.NO_ARGUMENTS)));
+ ClosureExpression readClosure = new ClosureExpression(new Parameter[]{}, readClosureCode);
+ readClosure.setVariableScope(new VariableScope());
+ Expression klass = new MethodCallExpression(VariableExpression.THIS_EXPRESSION, "getClass", MethodCallExpression.NO_ARGUMENTS);
+ Expression classLoader = new MethodCallExpression(klass, "getClassLoader", MethodCallExpression.NO_ARGUMENTS);
+ Expression result = new MethodCallExpression(bais, "withObjectInputStream", new ArgumentListExpression(classLoader, readClosure));
+ body.addStatement(new ReturnStatement(result));
+
+ ClassNode[] exceptions = {ClassHelper.make(CloneNotSupportedException.class)};
+ cNode.addMethod("clone", ACC_PUBLIC, ClassHelper.OBJECT_TYPE, new Parameter[0], exceptions, body);
+ }
+
+ private void createCloneCopyConstructor(ClassNode cNode, List list, List excludes) {
+ BlockStatement initBody = new BlockStatement();
+ if (cNode.getDeclaredConstructors().size() == 0) {
+ // add no-arg constructor
+ initBody.addStatement(new EmptyStatement());
+ cNode.addConstructor(ACC_PUBLIC, new Parameter[0], ClassNode.EMPTY_ARRAY, initBody);
+ initBody = new BlockStatement();
+ }
+ Parameter initParam = new Parameter(cNode, "other");
+ final Expression other = new VariableExpression(initParam);
+ boolean hasParent = cNode.getSuperClass() != ClassHelper.OBJECT_TYPE;
+ if (hasParent) {
+ initBody.addStatement(new ExpressionStatement(new ConstructorCallExpression(ClassNode.SUPER, other)));
+ }
+ for (FieldNode fieldNode : list) {
+ if (excludes.contains(fieldNode.getName())) continue;
+ PropertyExpression direct = new PropertyExpression(other, fieldNode.getName());
+ Expression cloned = new MethodCallExpression(direct, "clone", MethodCallExpression.NO_ARGUMENTS);
+ Expression to = new FieldExpression(fieldNode);
+ Statement assignCloned = new ExpressionStatement(new BinaryExpression(to, ASSIGN, cloned));
+ Statement assignDirect = new ExpressionStatement(new BinaryExpression(to, ASSIGN, direct));
+ initBody.addStatement(new IfStatement(isInstanceOf(direct, CLONEABLE_TYPE), assignCloned, assignDirect));
+ }
+ ClassNode[] exceptions = {ClassHelper.make(CloneNotSupportedException.class)};
+ cNode.addConstructor(ACC_PROTECTED, new Parameter[]{initParam}, ClassNode.EMPTY_ARRAY, initBody);
+ final BlockStatement cloneBody = new BlockStatement();
+ cloneBody.addStatement(new ExpressionStatement(new ConstructorCallExpression(cNode, new ArgumentListExpression(VariableExpression.THIS_EXPRESSION))));
+ cNode.addMethod("clone", ACC_PUBLIC, ClassHelper.OBJECT_TYPE, new Parameter[0], exceptions, cloneBody);
+ }
+
+ private void createClone(ClassNode cNode, List list, List excludes) {
+ final BlockStatement body = new BlockStatement();
+ final Expression result = new VariableExpression("_result");
+ final Expression clone = new MethodCallExpression(VariableExpression.SUPER_EXPRESSION, "clone", MethodCallExpression.NO_ARGUMENTS);
+ body.addStatement(new ExpressionStatement(new DeclarationExpression(result, ASSIGN, clone)));
+ for (FieldNode fieldNode : list) {
+ if (excludes.contains(fieldNode.getName())) continue;
+ Expression from = new MethodCallExpression(new FieldExpression(fieldNode), "clone", MethodCallExpression.NO_ARGUMENTS);
+ Expression to = new PropertyExpression(result, fieldNode.getName());
+ Statement doClone = new ExpressionStatement(new BinaryExpression(to, ASSIGN, from));
+ Statement doNothing = new EmptyStatement();
+ body.addStatement(new IfStatement(isInstanceOf(new FieldExpression(fieldNode), CLONEABLE_TYPE), doClone, doNothing));
+ }
+ body.addStatement(new ReturnStatement(result));
+ ClassNode[] exceptions = {ClassHelper.make(CloneNotSupportedException.class)};
+ cNode.addMethod("clone", ACC_PUBLIC, ClassHelper.OBJECT_TYPE, new Parameter[0], exceptions, body);
+ }
+
+ private AutoCloneStyle getStyle(AnnotationNode node, String name) {
+ final Expression member = node.getMember(name);
+ if (member != null && member instanceof PropertyExpression) {
+ PropertyExpression prop = (PropertyExpression) member;
+ Expression oe = prop.getObjectExpression();
+ if (oe instanceof ClassExpression) {
+ ClassExpression ce = (ClassExpression) oe;
+ if (ce.getType().getTypeClass() == AutoCloneStyle.class) {
+ return AutoCloneStyle.valueOf(prop.getPropertyAsString());
+ }
+ }
+ }
+ return null;
+ }
+
+}
Index: src/main/groovy/transform/AutoExternalize.java
===================================================================
--- src/main/groovy/transform/AutoExternalize.java (revision )
+++ src/main/groovy/transform/AutoExternalize.java (revision )
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2008-2010 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 groovy.transform;
+
+import org.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Note: This annotation is currently experimental! Use at your own risk!
+ *
+ * Class annotation used to assist in the creation of {@code Externalizable} classes.
+ * The {@code @AutoExternalize} annotation instructs the compiler to execute an
+ * AST transformation which adds {@code writeExternal()} and {@code readExternal()} methods
+ * to a class and adds {@code Externalizable} to the interfaces which the class implements.
+ * The {@code writeExternal()} method writes each property (or field) or the class while the
+ * {@code readExternal()} method will read each one in the same order. Properties or fields
+ * marked as {@code transient} are ignored.
+ *
+ * Example usage:
+ *
+ * import groovy.transform.*
+ * {@code @AutoExternalize}
+ * class Person {
+ * String first, last
+ * List favItems
+ * Date since
+ * }
+ *
+ * Which will create a class of the following form:
+ *
+ * class Person implements Externalizable {
+ * ...
+ * public void writeExternal(ObjectOutput out) throws IOException {
+ * out.writeObject(first)
+ * out.writeObject(last)
+ * out.writeObject(favItems)
+ * out.writeObject(since)
+ * }
+ *
+ * public void readExternal(ObjectInput oin) {
+ * first = oin.readObject()
+ * last = oin.readObject()
+ * favItems = oin.readObject()
+ * since = oin.readObject()
+ * }
+ * ...
+ * }
+ *
+ *
+ * @author Paul King
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.AutoExternalizeASTTransformation")
+public @interface AutoExternalize {
+ /**
+ * Comma separated list of property names to exclude from externalizing
+ */
+ String excludes() default "";
+
+ /**
+ * Include fields as well as properties when externalizing
+ */
+ boolean includeFields() default false;
+}
Index: pom.xml
===================================================================
--- pom.xml (revision 20310)
+++ pom.xml (revision )
@@ -543,6 +543,10 @@
Alberto Vilches Raton
+
+ Paulo Poiati
+
+
Index: src/main/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java
===================================================================
--- src/main/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java (revision )
+++ src/main/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java (revision )
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2008-2010 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.transform.EqualsAndHashCode;
+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.FieldNode;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.expr.*;
+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 java.util.ArrayList;
+import java.util.List;
+
+import static org.codehaus.groovy.transform.AbstractASTTransformUtil.*;
+
+@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
+public class EqualsAndHashCodeASTTransformation extends AbstractASTTransformation {
+ static final Class MY_CLASS = EqualsAndHashCode.class;
+ static final ClassNode MY_TYPE = new ClassNode(MY_CLASS);
+ static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
+ private static final ClassNode HASHUTIL_TYPE = new ClassNode(HashCodeHelper.class);
+ private static final Token ASSIGN = Token.newSymbol(Types.ASSIGN, -1, -1);
+ private static final ClassNode OBJECT_TYPE = new ClassNode(Object.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;
+ checkNotInterface(cNode, MY_TYPE_NAME);
+ boolean callSuper = memberHasValue(anno, "callSuper", true);
+ if (callSuper && cNode.getSuperClass().getName().equals("java.lang.Object")) {
+ addError("Error during " + MY_TYPE_NAME + " processing: callSuper=true but '" + cNode.getName() + "' has no super class.", anno);
+ }
+ boolean includeFields = memberHasValue(anno, "includeFields", true);
+ String rawExcludes = (String) getMemberValue(anno, "excludes");
+ List excludes = rawExcludes == null ? new ArrayList() : DefaultGroovyMethods.tokenize(rawExcludes, ", ");
+ createHashCode(cNode, false, includeFields, callSuper, excludes);
+ createEquals(cNode, includeFields, callSuper, excludes);
+ }
+ }
+
+ public static void createHashCode(ClassNode cNode, boolean cacheResult, boolean includeFields, boolean callSuper, List excludes) {
+ // 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 BlockStatement body = new BlockStatement();
+ List list = getInstancePropertyFields(cNode);
+ if (includeFields) {
+ list.addAll(getInstanceNonPropertyFields(cNode));
+ }
+ if (cacheResult) {
+ final FieldNode hashField = cNode.addField("$hash$code", ACC_PRIVATE | ACC_SYNTHETIC, ClassHelper.int_TYPE, null);
+ final Expression hash = new FieldExpression(hashField);
+ body.addStatement(new IfStatement(
+ isZeroExpr(hash),
+ calculateHashStatements(hash, list, callSuper),
+ new EmptyStatement()
+ ));
+ body.addStatement(new ReturnStatement(hash));
+ } else {
+ body.addStatement(calculateHashStatements(null, list, callSuper));
+ }
+
+ cNode.addMethod(new MethodNode(hasExistingHashCode ? "_hashCode" : "hashCode", hasExistingHashCode ? ACC_PRIVATE : ACC_PUBLIC,
+ ClassHelper.int_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body));
+ }
+
+ private static Statement calculateHashStatements(Expression hash, List list, boolean callSuper) {
+ 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)));
+
+ for (FieldNode fNode : list) {
+ if (fNode.getName().contains("$")) continue;
+ // _result = HashCodeHelper.updateHash(_result, field)
+ final Expression fieldExpr = new FieldExpression(fNode);
+ final Expression args = new TupleExpression(result, fieldExpr);
+ final Expression current = new StaticMethodCallExpression(HASHUTIL_TYPE, "updateHash", args);
+ body.addStatement(assignStatement(result, current));
+ }
+ if (callSuper) {
+ // _result = HashCodeHelper.updateHash(_result, super.hashCode())
+ final Expression args = new TupleExpression(result, new MethodCallExpression(VariableExpression.SUPER_EXPRESSION, "hashCode", MethodCallExpression.NO_ARGUMENTS));
+ final Expression current = new StaticMethodCallExpression(HASHUTIL_TYPE, "updateHash", args);
+ body.addStatement(assignStatement(result, current));
+ }
+ // $hash$code = _result
+ if (hash != null) {
+ body.addStatement(assignStatement(hash, result));
+ } else {
+ body.addStatement(new ReturnStatement(result));
+ }
+ return body;
+ }
+
+ public static void createEquals(ClassNode cNode, boolean includeFields, boolean callSuper, List excludes) {
+ // 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));
+
+ body.addStatement(new ExpressionStatement(new BinaryExpression(other, ASSIGN, new CastExpression(cNode, other))));
+
+ List list = getInstancePropertyFields(cNode);
+ if (includeFields) {
+ list.addAll(getInstanceNonPropertyFields(cNode));
+ }
+ for (FieldNode fNode : list) {
+ if (excludes.contains(fNode.getName()) || fNode.getName().contains("$")) continue;
+ body.addStatement(returnFalseIfPropertyNotEqual(fNode, other));
+ }
+ if (callSuper) {
+ Statement result = new IfStatement(
+ isTrueExpr(new MethodCallExpression(VariableExpression.SUPER_EXPRESSION, "equals", other)),
+ new EmptyStatement(),
+ new ReturnStatement(ConstantExpression.FALSE)
+ );
+ body.addStatement(result);
+ }
+
+ // 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));
+ }
+}
Index: gradle/pomconfigurer.gradle
===================================================================
--- gradle/pomconfigurer.gradle (revision 20093)
+++ gradle/pomconfigurer.gradle (revision )
@@ -433,7 +433,10 @@
contributor {
name 'Alberto Vilches Raton'
}
+ contributor {
+ name 'Paulo Poiati'
- }
+ }
+ }
mailingLists {
mailingList {
name 'Groovy JSR Discussion List'