http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/ToString.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/ToString.java b/src/main/groovy/groovy/transform/ToString.java new file mode 100644 index 0000000..25fccab --- /dev/null +++ b/src/main/groovy/groovy/transform/ToString.java @@ -0,0 +1,339 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package 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. + * <p> + * It allows you to write classes in this shortened form: + * <pre> + * {@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']) + * </pre> + * Which will have this output: + * <pre> + * Customer(Tom, Jones, 21, Wed Jul 14 23:57:14 EST 2010, [Books, Games]) + * </pre> + * There are numerous options to customize the format of the generated output. + * E.g. if you change the first annotation to: + * <pre> + * {@code @ToString(includeNames=true)} + * </pre> + * Then the output will be: + * <pre> + * Customer(first:Tom, last:Jones, age:21, since:Wed Jul 14 23:57:50 EST 2010, favItems:[Books, Games]) + * </pre> + * Or if you change the first annotation to: + * <pre> + * {@code @ToString(includeNames=true,includeFields=true,excludes="since,favItems")} + * </pre> + * Then the output will be: + * <pre> + * Customer(first:Tom, last:Jones, age:21, answer:42) + * </pre> + * If you have this example: + * <pre class="groovyTestCase"> + * import groovy.transform.ToString + * {@code @ToString} class NamedThing { + * String name + * } + * {@code @ToString}(includeNames=true,includeSuper=true) + * class AgedThing extends NamedThing { + * int age + * } + * String agedThingAsString = new AgedThing(name:'Lassie', age:5).toString() + * assert agedThingAsString == 'AgedThing(age:5, super:NamedThing(Lassie))' + * </pre> + * {@code @ToString} can also be used in conjunction with {@code @Canonical} and {@code @Immutable}. + * <p> + * If you want to omit fields or properties referring to <tt>null</tt>, you can use the <tt>ignoreNulls</tt> flag: + * <pre class="groovyTestCase"> + * import groovy.transform.ToString + * {@code @ToString(ignoreNulls = true)} class NamedThing { + * String name + * } + * assert new NamedThing(name: null).toString() == 'NamedThing()' + * </pre> + * <p> + * By default the fully-qualified class name is used as part of the generated toString. + * If you want to exclude the package, you can set the includePackage flag to false, e.g.: + * <pre> + * package my.company + * import groovy.transform.ToString + * {@code @ToString(includePackage = false)} class NamedThing { + * String name + * } + * println new NamedThing(name: "Lassie") + * </pre> + * Which results in: + * <pre> + * NamedThing(name: Lassie) + * </pre> + * If the includePackage flag is {@code true} (the default), then the output will be: + * <pre> + * my.company.NamedThing(name: Lassie) + * </pre> + * <p>More examples:</p> + * <pre class="groovyTestCase"> + * //-------------------------------------------------------------------------- + * // Most simple implementation of toString. + * import groovy.transform.ToString + * + * {@code @ToString} + * class Person { + * String name + * List likes + * private boolean active = false + * } + * + * def person = new Person(name: 'mrhaki', likes: ['Groovy', 'Java']) + * + * assert person.toString() == 'Person(mrhaki, [Groovy, Java])' + * </pre> + * <pre class="groovyTestCase"> + * //-------------------------------------------------------------------------- + * // includeNames to output the names of the properties. + * import groovy.transform.ToString + * + * @ToString(includeNames=true) + * class Person { + * String name + * List likes + * private boolean active = false + * } + * + * def person = new Person(name: 'mrhaki', likes: ['Groovy', 'Java']) + * + * assert person.toString() == 'Person(name:mrhaki, likes:[Groovy, Java])' + * </pre> + * <pre class="groovyTestCase"> + * //-------------------------------------------------------------------------- + * // includeFields to not only output properties, but also field values. + * import groovy.transform.ToString + * + * @ToString(includeNames=true, includeFields=true) + * class Person { + * String name + * List likes + * private boolean active = false + * } + * + * def person = new Person(name: 'mrhaki', likes: ['Groovy', 'Java']) + * + * assert person.toString() == 'Person(name:mrhaki, likes:[Groovy, Java], active:false)' + * </pre> + * <pre> + * //-------------------------------------------------------------------------- + * // Use includeSuper to include properties from super class in output. + * import groovy.transform.ToString + * + * @ToString(includeNames=true) + * class Person { + * String name + * List likes + * private boolean active = false + * } + * + * @ToString(includeSuper=true, includeNames=true) + * class Student extends Person { + * List courses + * } + * + * def student = new Student(name: 'mrhaki', likes: ['Groovy', 'Java'], courses: ['IT', 'Business']) + * + * assert student.toString() == 'Student(courses:[IT, Business], super:Person(name:mrhaki, likes:[Groovy, Java]))' + * </pre> + * <pre class="groovyTestCase"> + * //-------------------------------------------------------------------------- + * // excludes active field and likes property from output + * import groovy.transform.ToString + * + * @ToString(includeNames=true, includeFields=true, excludes='active,likes') + * class Person { + * String name + * List likes + * private boolean active = false + * } + * + * def person = new Person(name: 'mrhaki', likes: ['Groovy', 'Java']) + * + * assert person.toString() == 'Person(name:mrhaki)' + * </pre> + * <pre class="groovyTestCase"> + * //-------------------------------------------------------------------------- + * // Don't include the package name in the output + * package com.mrhaki.blog.groovy + * + * import groovy.transform.* + * + * @ToString(includePackage=false) + * class Course { + * String title + * Integer maxAttendees + * } + * + * final Course course = new Course(title: 'Groovy 101', maxAttendees: 200) + * + * assert course.toString() == 'Course(Groovy 101, 200)' + * </pre> + * <pre class="groovyTestCase"> + * //-------------------------------------------------------------------------- + * // Don't use properties with null value. + * package com.mrhaki.blog.groovy + * + * import groovy.transform.* + * + * @ToString(ignoreNulls=true) + * class Course { + * String title + * Integer maxAttendees + * } + * + * final Course course = new Course(title: 'Groovy 101') + * + * assert course.toString() == 'com.mrhaki.blog.groovy.Course(Groovy 101)' + * </pre> + * <pre class="groovyTestCase"> + * //-------------------------------------------------------------------------- + * // Cache toString() result. + * package com.mrhaki.blog.groovy + * + * import groovy.transform.* + * + * @ToString(cache=true) + * class Course { + * String title + * Integer maxAttendees + * } + * + * Course course = new Course(title: 'Groovy 101', maxAttendees: 200) + * + * assert course.toString() == 'com.mrhaki.blog.groovy.Course(Groovy 101, 200)' + * + * // Value change will not be reflected in toString(). + * course.title = 'Grails with REST' + * + * assert course.toString() == 'com.mrhaki.blog.groovy.Course(Groovy 101, 200)' + * assert course.title == 'Grails with REST' + * </pre> + * + * @author Paul King + * @author Andre Steingress + * @see groovy.transform.Immutable + * @see groovy.transform.Canonical + * @since 1.8.0 + */ [email protected] +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.ToStringASTTransformation") +public @interface ToString { + /** + * List of field and/or property names to exclude from generated toString. + * Must not be used if 'includes' is used. For convenience, a String with comma separated names + * can be used in addition to an array (using Groovy's literal list notation) of String values. + */ + String[] excludes() default {}; + + /** + * List of field and/or property names to include within the generated toString. + * Must not be used if 'excludes' is used. For convenience, a String with comma separated names + * can be used in addition to an array (using Groovy's literal list notation) of String values. + * The default value is a special marker value indicating that no includes are defined; all fields and/or properties + * are included if 'includes' remains undefined and 'excludes' is explicitly or implicitly an empty list. + * The special name 'super' can be used instead of using the 'includeSuper' flag. + */ + String[] includes() default {Undefined.STRING}; + + /** + * Whether to include the toString() of super in the generated toString. + */ + boolean includeSuper() default false; + + /** + * Whether to include super properties in the generated toString. + * @since 2.4.0 + */ + boolean includeSuperProperties() default false; + + /** + * Whether to include names of properties/fields in the generated toString. + */ + boolean includeNames() default false; + + /** + * Include fields as well as properties in the generated toString. + */ + boolean includeFields() default false; + + /** + * Don't display any fields or properties with value <tt>null</tt>. + */ + boolean ignoreNulls() default false; + + /** + * Whether to include the fully-qualified class name (i.e. including + * the package) or just the simple class name in the generated toString. + * @since 2.0.6 + */ + boolean includePackage() default true; + + /** + * Whether to include all properties (as per the JavaBean spec) in the generated toString. + * Groovy recognizes any field-like definitions with no explicit visibility as property definitions + * and always includes them in the {@code @ToString} generated toString (as well as auto-generating the + * appropriate getters and setters). Groovy also treats any explicitly created getXxx() or isYyy() + * methods as property getters as per the JavaBean specification. Old versions of Groovy did not. + * So set this flag to false for the old behavior or if you want to explicitly exclude such properties. + * + * @since 2.5.0 + */ + boolean allProperties() default true; + + /** + * Whether to cache toString() calculations. You should only set this to true if + * you know the object is immutable (or technically mutable but never changed). + * @since 2.1.0 + */ + boolean cache() default false; + + /** + * Whether to include all fields and/or properties in the generated toString, including those with names that + * are considered internal. + * + * @since 2.5.0 + */ + boolean allNames() default false; +}
http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/Trait.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/Trait.java b/src/main/groovy/groovy/transform/Trait.java new file mode 100644 index 0000000..de88180 --- /dev/null +++ b/src/main/groovy/groovy/transform/Trait.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package 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; + +/** + * Used to mark a class as being a trait. + * + * @since 2.3.0 + */ [email protected] +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.trait.TraitASTTransformation") +public @interface Trait { +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/TupleConstructor.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/TupleConstructor.java b/src/main/groovy/groovy/transform/TupleConstructor.java new file mode 100644 index 0000000..2cd2be7 --- /dev/null +++ b/src/main/groovy/groovy/transform/TupleConstructor.java @@ -0,0 +1,278 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package 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. + * Should be used with care with other annotations which create constructors - see "Known + * Limitations" for more details. + * <p> + * It allows you to write classes in this shortened form: + * <pre class="groovyTestCase"> + * {@code @groovy.transform.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') + * </pre> + * The {@code @TupleConstructor} annotation instructs the compiler to execute an + * AST transformation which adds the necessary constructor method to your class. + * <p> + * A tuple constructor is created with a parameter for each property (and optionally field and + * super properties). + * A default value is provided (using Java's default values) for all parameters in the constructor. + * Groovy's normal conventions then allows any number of parameters to be left off the end of the parameter list + * including all of the parameters - giving a no-arg constructor which can be used with the map-style naming conventions. + * <p> + * The order of parameters is given by the properties of any super classes with most super first + * (if {@code includeSuperProperties} is set) followed by the properties of the class followed + * by the fields of the class (if {@code includeFields} is set). Within each grouping the order + * is as attributes appear within the respective class. + * <p>More examples:</p> + * <pre class="groovyTestCase"> + * //-------------------------------------------------------------------------- + * import groovy.transform.TupleConstructor + * + * @TupleConstructor() + * class Person { + * String name + * List likes + * private boolean active = false + * } + * + * def person = new Person('mrhaki', ['Groovy', 'Java']) + * + * assert person.name == 'mrhaki' + * assert person.likes == ['Groovy', 'Java'] + * + * person = new Person('mrhaki') + * + * assert person.name == 'mrhaki' + * assert !person.likes + * </pre> + * <pre class="groovyTestCase"> + * //-------------------------------------------------------------------------- + * // includeFields in the constructor creation. + * import groovy.transform.TupleConstructor + * + * @TupleConstructor(includeFields=true) + * class Person { + * String name + * List likes + * private boolean active = false + * + * boolean isActivated() { active } + * } + * + * def person = new Person('mrhaki', ['Groovy', 'Java'], true) + * + * assert person.name == 'mrhaki' + * assert person.likes == ['Groovy', 'Java'] + * assert person.activated + * </pre> + * <pre class="groovyTestCase"> + * //-------------------------------------------------------------------------- + * // use force attribute to force creation of constructor + * // even if we define our own constructors. + * import groovy.transform.TupleConstructor + * + * @TupleConstructor(force=true) + * class Person { + * String name + * List likes + * private boolean active = false + * + * Person(boolean active) { + * this.active = active + * } + * + * boolean isActivated() { active } + * } + * + * def person = new Person('mrhaki', ['Groovy', 'Java']) + * + * assert person.name == 'mrhaki' + * assert person.likes == ['Groovy', 'Java'] + * assert !person.activated + * + * person = new Person(true) + * + * assert person.activated + * </pre> + * <pre class="groovyTestCase"> + * //-------------------------------------------------------------------------- + * // include properties and fields from super class. + * import groovy.transform.TupleConstructor + * + * @TupleConstructor(includeFields=true) + * class Person { + * String name + * List likes + * private boolean active = false + * + * boolean isActivated() { active } + * } + * + * @TupleConstructor(callSuper=true, includeSuperProperties=true, includeSuperFields=true) + * class Student extends Person { + * List courses + * } + * + * def student = new Student('mrhaki', ['Groovy', 'Java'], true, ['IT']) + * + * assert student.name == 'mrhaki' + * assert student.likes == ['Groovy', 'Java'] + * assert student.activated + * assert student.courses == ['IT'] + * </pre> + * <p> + * Known Limitations: + * <ul> + * <li>This AST transform might become a no-op if you are defining your own constructors or + * combining with other AST transforms which create constructors (e.g. {@code @InheritConstructors}); + * the order in which the particular transforms are processed becomes important in that case. + * See the {@code force} attribute for further details about customizing this behavior.</li> + * <li>This AST transform normally uses default parameter values which creates multiple constructors under + * the covers. You should use with care if you are defining your own constructors or + * combining with other AST transforms which create constructors (e.g. {@code @InheritConstructors}); + * the order in which the particular transforms are processed becomes important in that case. + * See the {@code defaults} attribute for further details about customizing this behavior.</li> + * <li>Groovy's normal map-style naming conventions will not be available if the first property (or field) + * has type {@code LinkedHashMap} or if there is a single Map, AbstractMap or HashMap property (or field)</li> + * </ul> + * + * @since 1.8.0 + */ [email protected] +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.TYPE}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.TupleConstructorASTTransformation") +public @interface TupleConstructor { + /** + * List of field and/or property names to exclude from the constructor. + * Must not be used if 'includes' is used. For convenience, a String with comma separated names + * can be used in addition to an array (using Groovy's literal list notation) of String values. + */ + String[] excludes() default {}; + + /** + * List of field and/or property names to include within the constructor. + * Must not be used if 'excludes' is used. For convenience, a String with comma separated names + * can be used in addition to an array (using Groovy's literal list notation) of String values. + * The default value is a special marker value indicating that no includes are defined; + * all fields are included if includes remains undefined and excludes is explicitly or implicitly + * an empty list. + */ + String[] includes() default {Undefined.STRING}; + + /** + * Include fields in the constructor. + */ + boolean includeFields() default false; + + /** + * Include properties in the constructor. + */ + boolean includeProperties() default true; + + /** + * Include fields from super classes in the constructor. + */ + boolean includeSuperFields() default false; + + /** + * Include properties from super classes in the constructor. + */ + boolean includeSuperProperties() default false; + + /** + * Should super properties be called within a call to the parent constructor + * rather than set as properties. Typically used in combination with {@code includeSuperProperties}. + * Can't be true if using {@code pre} with a {@code super} first statement. + */ + boolean callSuper() 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; + + /** + * Used to set whether default value processing is enabled (the default) or disabled. + * + * By default, every constructor parameter is given a default value. This value will + * be Java's default for primitive types (zero or false) and null for Objects, unless + * an initial value is given when declaring the property or field. A consequence of + * this design is that you can leave off parameters from the right if the default + * value will suffice. As far as Java interoperability is concerned, Groovy will + * create additional constructors under the covers representing the constructors + * with parameters left off, all the way from the constructor with all arguments + * to the no-arg constructor. + * + * However, when set to false, default values are not allowed for properties and fields. + * Only the constructor containing all arguments will be provided. + * In particular, a no-arg constructor won't be provided and since this is currently + * used by Groovy when using named-arguments, the named-argument style won't be available. + */ + boolean defaults() default true; + + /** + * By default, properties are set directly using their respective field. + * By setting {@code useSetters=true} then a writable property will be set using its setter. + * If turning on this flag we recommend that setters that might be called are + * made null-safe wrt the parameter. + */ + boolean useSetters() default false; + + /** + * Whether to include all fields and/or properties within the constructor, including those with names that are + * considered internal. + * + * @since 2.5.0 + */ + boolean allNames() default false; + + /** + * A Closure containing statements which will be prepended to the generated constructor. The first statement + * within the Closure may be {@code super(someArgs)} in which case the no-arg super constructor won't be called. + * + * @since 2.5.0 + */ + Class pre() default Undefined.CLASS.class; + + /** + * A Closure containing statements which will be appended to the end of the generated constructor. Useful for validation steps or tweaking the populated fields/properties. + * + * @since 2.5.0 + */ + Class post() default Undefined.CLASS.class; +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/TypeChecked.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/TypeChecked.java b/src/main/groovy/groovy/transform/TypeChecked.java new file mode 100644 index 0000000..b902f3f --- /dev/null +++ b/src/main/groovy/groovy/transform/TypeChecked.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package 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; + +/** + * This will let the Groovy compiler use compile time checks in the style of Java. + * @author <a href="mailto:[email protected]">Jochen "blackdrag" Theodorou</a> + */ [email protected] +@Retention(RetentionPolicy.SOURCE) +@Target({ ElementType.METHOD, ElementType.TYPE, + ElementType.CONSTRUCTOR +}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.StaticTypesTransformation") +public @interface TypeChecked { + TypeCheckingMode value() default TypeCheckingMode.PASS; + + /** + * The list of (classpath resources) paths to type checking DSL scripts, also known + * as type checking extensions. + * @return an array of paths to groovy scripts that must be on compile classpath + */ + String[] extensions() default {}; + + /** + * This annotation is added by @TypeChecked on methods which have type checking turned on. + * It is used to embed type information into binary, so that the type checker can use this information, + * if available, for precompiled classes. + */ + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface TypeCheckingInfo { + /** + * Returns the type checker information protocol number. This is used if the format of the + * string used in {@link #inferredType()} changes. + * @return the protocol version + */ + int version() default 0; + + /** + * An encoded type information. + * @return the inferred type + */ + String inferredType(); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/TypeCheckingMode.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/TypeCheckingMode.java b/src/main/groovy/groovy/transform/TypeCheckingMode.java new file mode 100644 index 0000000..075bd71 --- /dev/null +++ b/src/main/groovy/groovy/transform/TypeCheckingMode.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform; + +/** + * This enumeration can be used whenever it is preferred to annotate a class as + * {@link TypeChecked} in general, but where only one or more methods are "dynamic". This allows the user + * to annotate the class itself then annotate only the methods which require exclusion. + * + * @author Cedric Champeau + */ +public enum TypeCheckingMode { + PASS, + SKIP +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/Undefined.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/Undefined.java b/src/main/groovy/groovy/transform/Undefined.java new file mode 100644 index 0000000..35b360d --- /dev/null +++ b/src/main/groovy/groovy/transform/Undefined.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform; + +import org.codehaus.groovy.ast.ClassNode; + +/** + * Java doesn't allow you to have null as an attribute value. It wants you to indicate what you really + * mean by null, so that is what we do here - as ugly as it is. + */ +public final class Undefined { + private Undefined() {} + public static final String STRING = "<DummyUndefinedMarkerString-DoNotUse>"; + public static final class CLASS {} + public static final class EXCEPTION extends RuntimeException { + private static final long serialVersionUID = -3960500360386581172L; + } + public static boolean isUndefined(String other) { return STRING.equals(other); } + public static boolean isUndefined(ClassNode other) { return CLASS.class.getName().equals(other.getName()); } + public static boolean isUndefinedException(ClassNode other) { return EXCEPTION.class.getName().equals(other.getName()); } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/WithReadLock.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/WithReadLock.java b/src/main/groovy/groovy/transform/WithReadLock.java new file mode 100644 index 0000000..475786a --- /dev/null +++ b/src/main/groovy/groovy/transform/WithReadLock.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package 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; + +/** + * This annotation is used in conjunction with {@link WithWriteLock} to support read and write synchronization on a method. + * <p> + * To use this annotation, declare {@code @WithReadLock} on your method. The method may be either an instance method or + * a static method. The resulting method will allow multiple threads to read the information at the same time. + * However, if some other method obtains a write lock, then this method will force callers to wait until the write is complete. + * <p> + * This annotation is a declarative wrapper around the JDK's <code>java.util.concurrent.locks.ReentrantReadWriteLock</code>. + * Objects containing this annotation will have a ReentrantReadWriteLock field named <code>$reentrantLock</code> added to the class, + * and method access is protected by the lock. If the method is static then the field is static and named <code>$REENTRANTLOCK</code>. + * <p> + * The annotation takes an optional parameter for the name of the field. This field must exist on the class and must be + * of type ReentrantReadWriteLock. + * <p> + * To understand how this annotation works, it is convenient to think in terms of the source code it replaces. The following + * is a typical usage of this annotation from Groovy: + * <pre> + * import groovy.transform.*; + * + * public class ResourceProvider { + * + * private final Map<String, String> data = new HashMap<String, String>(); + * + * {@code @WithReadLock} + * public String getResource(String key) throws Exception { + * return data.get(key); + * } + * + * {@code @WithWriteLock} + * public void refresh() throws Exception { + * //reload the resources into memory + * } + * } + * </pre> + * As part of the Groovy compiler, code resembling this is produced: + * <pre> + * import java.util.concurrent.locks.ReentrantReadWriteLock; + * import java.util.concurrent.locks.ReadWriteLock; + * + * public class ResourceProvider { + * + * private final ReadWriteLock $reentrantlock = new ReentrantReadWriteLock(); + * private final Map<String, String> data = new HashMap<String, String>(); + * + * public String getResource(String key) throws Exception { + * $reentrantlock.readLock().lock(); + * try { + * return data.get(key); + * } finally { + * $reentrantlock.readLock().unlock(); + * } + * } + * + * public void refresh() throws Exception { + * $reentrantlock.writeLock().lock(); + * try { + * //reload the resources into memory + * } finally { + * $reentrantlock.writeLock().unlock(); + * } + * } + * } + * </pre> + * + * @author Hamlet D'Arcy + * @since 1.8.0 + */ [email protected] +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.ReadWriteLockASTTransformation") +public @interface WithReadLock { + /** + * @return if a user specified lock object with the given name should be used + * the lock object must exist. If the annotated method is static then the + * lock object must be static. If the annotated method is not static then + * the lock object must not be static. + */ + String value () default ""; +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/WithWriteLock.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/WithWriteLock.java b/src/main/groovy/groovy/transform/WithWriteLock.java new file mode 100644 index 0000000..1eeb7f0 --- /dev/null +++ b/src/main/groovy/groovy/transform/WithWriteLock.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package 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; + +/** + * This annotation is used in conjunction with {@link WithReadLock} to support read and write synchronization on a method. + * <p> + * To use this annotation, declare {@code @WithWriteLock} on your method. The method may be either an instance method or + * a static method. The resulting method will allow only one thread access to the method at a time, and will wait to access + * the method until any other read locks have been released. + * <p> + * This annotation is a declarative wrapper around the JDK's <code>java.util.concurrent.locks.ReentrantReadWriteLock</code>. + * Objects containing this annotation will have a ReentrantReadWriteLock field named <code>$reentrantLock</code> added to the class, + * and method access is protected by the lock. If the method is static then the field is static and named <code>$REENTRANTLOCK</code>. + * <p> + * The annotation takes an optional parameter for the name of the field. This field must exist on the class and must be + * of type ReentrantReadWriteLock. + * <p> + * To understand how this annotation works, it is convenient to think in terms of the source code it replaces. The following + * is a typical usage of this annotation from Groovy: + * <pre> + * import groovy.transform.*; + * + * public class ResourceProvider { + * + * private final Map<String, String> data = new HashMap<String, String>(); + * + * {@code @WithReadLock} + * public String getResource(String key) throws Exception { + * return data.get(key); + * } + * + * {@code @WithWriteLock} + * public void refresh() throws Exception { + * //reload the resources into memory + * } + * } + * </pre> + * As part of the Groovy compiler, code resembling this is produced: + * <pre> + * import java.util.concurrent.locks.ReentrantReadWriteLock; + * import java.util.concurrent.locks.ReadWriteLock; + * + * public class ResourceProvider { + * + * private final ReadWriteLock $reentrantlock = new ReentrantReadWriteLock(); + * private final Map<String, String> data = new HashMap<String, String>(); + * + * public String getResource(String key) throws Exception { + * $reentrantlock.readLock().lock(); + * try { + * return data.get(key); + * } finally { + * $reentrantlock.readLock().unlock(); + * } + * } + * + * public void refresh() throws Exception { + * $reentrantlock.writeLock().lock(); + * try { + * //reload the resources into memory + * } finally { + * $reentrantlock.writeLock().unlock(); + * } + * } + * } + * </pre> + * + * @author Hamlet D'Arcy + * @since 1.8.0 + */ [email protected] +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.ReadWriteLockASTTransformation") +public @interface WithWriteLock { + /** + * @return if a user specified lock object with the given name should be used + * the lock object must exist. If the annotated method is static then the + * lock object must be static. If the annotated method is not static then + * the lock object must not be static. + */ + String value () default ""; +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/builder/Builder.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/builder/Builder.java b/src/main/groovy/groovy/transform/builder/Builder.java new file mode 100644 index 0000000..93b6090 --- /dev/null +++ b/src/main/groovy/groovy/transform/builder/Builder.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform.builder; + +import groovy.transform.Undefined; +import org.codehaus.groovy.transform.GroovyASTTransformationClass; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static org.codehaus.groovy.transform.BuilderASTTransformation.BuilderStrategy; + +/** + * The {@code @Builder} AST transformation is used to help write classes that can be created using <em>fluent</em> api calls.<!-- --> + * The transform supports multiple building strategies to cover a range of cases and there are a number + * of configuration options to customize the building process. + * + * In addition, a number of annotation attributes let you customise the building process. Not all annotation attributes + * are supported by all strategies. See the individual strategy documentation for more details. + * If you're an AST hacker, you can also define your own strategy class. + * + * The following strategies are bundled with Groovy: + * <ul> + * <li>{@link SimpleStrategy} for creating chained setters</li> + * <li>{@link ExternalStrategy} where you annotate an explicit builder class while leaving some buildee class being built untouched</li> + * <li>{@link DefaultStrategy} which creates a nested helper class for instance creation</li> + * <li>{@link InitializerStrategy} which creates a nested helper class for instance creation which when used with {@code @CompileStatic} allows type-safe object creation</li> + * </ul> + * + * Note that Groovy provides other built-in mechanisms for easy creation of objects, e.g. the named-args constructor: + * <pre> + * new Person(firstName: "Robert", lastName: "Lewandowski", age: 21) + * </pre> + * or the with statement: + * <pre> + * new Person().with { + * firstName = "Robert" + * lastName = "Lewandowski" + * age = 21 + * } + * </pre> + * so you might not find value in using the builder transform at all. But if you need Java integration or in some cases improved type safety, the {@code @Builder} transform might prove very useful. + * + * @see groovy.transform.builder.SimpleStrategy + * @see groovy.transform.builder.ExternalStrategy + * @see groovy.transform.builder.DefaultStrategy + * @see groovy.transform.builder.InitializerStrategy + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.BuilderASTTransformation") +public @interface Builder { + + /** + * A class for which builder methods should be created. It will be an error to leave + * this attribute with its default value for some strategies. + */ + Class forClass() default Undefined.CLASS.class; + + /** + * A class capturing the builder strategy + */ + Class<? extends BuilderStrategy> builderStrategy() default DefaultStrategy.class; + + /** + * The prefix to use when creating the setter methods. + * Default is determined by the strategy which might use "" or "set" but you can choose your own, e.g. "with". + * If non-empty the first letter of the property will be capitalized before being appended to the prefix. + */ + String prefix() default Undefined.STRING; + + /** + * For strategies which create a builder helper class, the class name to use for the helper class. + * Not used if using {@code forClass} since in such cases the builder class is explicitly supplied. + * Default is determined by the strategy, e.g. <em>TargetClass</em> + "Builder" or <em>TargetClass</em> + "Initializer". + */ + String builderClassName() default Undefined.STRING; + + /** + * For strategies which create a builder helper class that creates the instance, the method name to call to create the instance. + * Default is determined by the strategy, e.g. <em>build</em> or <em>create</em>. + */ + String buildMethodName() default Undefined.STRING; + + /** + * The method name to use for a builder factory method in the source class for easy access of the + * builder helper class for strategies which create such a helper class. + * Must not be used if using {@code forClass}. + * Default is determined by the strategy, e.g. <em>builder</em> or <em>createInitializer</em>. + */ + String builderMethodName() default Undefined.STRING; + + /** + * List of field and/or property names to exclude from generated builder methods. + * Must not be used if 'includes' is used. For convenience, a String with comma separated names + * can be used in addition to an array (using Groovy's literal list notation) of String values. + */ + String[] excludes() default {}; + + /** + * List of field and/or property names to include within the generated builder methods. + * Must not be used if 'excludes' is used. For convenience, a String with comma separated names + * can be used in addition to an array (using Groovy's literal list notation) of String values. + * The default value is a special marker value indicating that no includes are defined; all fields + * are included if includes remains undefined and excludes is explicitly or implicitly an empty list. + */ + String[] includes() default {Undefined.STRING}; + + /** + * By default, properties are set directly using their respective field. + * By setting {@code useSetters=true} then a writable property will be set using its setter. + * If turning on this flag we recommend that setters that might be called are + * made null-safe wrt the parameter. + */ + boolean useSetters() default false; + + /** + * Generate builder methods for properties from super classes. + */ + boolean includeSuperProperties() default false; + + /** + * Whether the generated builder should support all properties, including those with names that are considered internal. + * + * @since 2.5.0 + */ + boolean allNames() default false; + + /** + * Whether to include all properties (as per the JavaBean spec) in the generated builder. + * Groovy recognizes any field-like definitions with no explicit visibility as property definitions + * and always includes them in the {@code @Builder} generated classes. Groovy also treats any explicitly created getXxx() or isYyy() + * methods as property getters as per the JavaBean specification. Old versions of Groovy did not. + * So set this flag to false for the old behavior or if you want to explicitly exclude such properties. + * Currently only supported by DefaultStrategy and ExternalStrategy. + * + * @since 2.5.0 + */ + boolean allProperties() default true; +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/builder/DefaultStrategy.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/builder/DefaultStrategy.java b/src/main/groovy/groovy/transform/builder/DefaultStrategy.java new file mode 100644 index 0000000..65d90e3 --- /dev/null +++ b/src/main/groovy/groovy/transform/builder/DefaultStrategy.java @@ -0,0 +1,293 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform.builder; + +import groovy.transform.Undefined; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ConstructorNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.InnerClassNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.transform.BuilderASTTransformation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE; +import static org.codehaus.groovy.ast.tools.GeneralUtils.args; +import static org.codehaus.groovy.ast.tools.GeneralUtils.assignX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.block; +import static org.codehaus.groovy.ast.tools.GeneralUtils.callX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.constX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.declS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.param; +import static org.codehaus.groovy.ast.tools.GeneralUtils.params; +import static org.codehaus.groovy.ast.tools.GeneralUtils.propX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt; +import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; +import static org.codehaus.groovy.ast.tools.GenericsUtils.correctToGenericsSpecRecurse; +import static org.codehaus.groovy.ast.tools.GenericsUtils.createGenericsSpec; +import static org.codehaus.groovy.ast.tools.GenericsUtils.extractSuperClassGenerics; +import static org.codehaus.groovy.ast.tools.GenericsUtils.newClass; +import static org.codehaus.groovy.transform.AbstractASTTransformation.getMemberStringValue; +import static org.codehaus.groovy.transform.BuilderASTTransformation.NO_EXCEPTIONS; +import static org.codehaus.groovy.transform.BuilderASTTransformation.NO_PARAMS; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_STATIC; + +/** + * This strategy is used with the {@link Builder} AST transform to create a builder helper class + * for the fluent creation of instances of a specified class. It can be used at the class, + * static method or constructor levels. + * + * You use it as follows: + * <pre class="groovyTestCase"> + * import groovy.transform.builder.* + * + * {@code @Builder} + * class Person { + * String firstName + * String lastName + * int age + * } + * def person = Person.builder().firstName("Robert").lastName("Lewandowski").age(21).build() + * assert person.firstName == "Robert" + * assert person.lastName == "Lewandowski" + * assert person.age == 21 + * </pre> + * The {@code prefix} annotation attribute can be used to create setters with a different naming convention. The default is the + * empty string but you could change that to "set" as follows: + * <pre class="groovyTestCase"> + * {@code @groovy.transform.builder.Builder}(prefix='set') + * class Person { + * String firstName + * String lastName + * int age + * } + * def p2 = Person.builder().setFirstName("Robert").setLastName("Lewandowski").setAge(21).build() + * </pre> + * or using a prefix of 'with' would result in usage like this: + * <pre> + * def p3 = Person.builder().withFirstName("Robert").withLastName("Lewandowski").withAge(21).build() + * </pre> + * + * You can also use the {@code @Builder} annotation in combination with this strategy on one or more constructor or + * static method instead of or in addition to using it at the class level. An example with a constructor follows: + * <pre class="groovyTestCase"> + * import groovy.transform.ToString + * import groovy.transform.builder.Builder + * + * {@code @ToString} + * class Person { + * String first, last + * int born + * + * {@code @Builder} + * Person(String roleName) { + * if (roleName == 'Jack Sparrow') { + * first = 'Johnny'; last = 'Depp'; born = 1963 + * } + * } + * } + * assert Person.builder().roleName("Jack Sparrow").build().toString() == 'Person(Johnny, Depp, 1963)' + * </pre> + * In this case, the parameter(s) for the constructor or static method become the properties available + * in the builder. For the case of a static method, the return type of the static method becomes the + * class of the instance being created. For static factory methods, this is normally the class containing the + * static method but in general it can be any class. + * + * Note: if using more than one {@code @Builder} annotation, which is only possible when using static method + * or constructor variants, it is up to you to ensure that any generated helper classes or builder methods + * have unique names. E.g. we can modify the previous example to have three builders. At least two of the builders + * in our case will need to set the 'builderClassName' and 'builderMethodName' annotation attributes to ensure + * we have unique names. This is shown in the following example: + * <pre class="groovyTestCase"> + * import groovy.transform.builder.* + * import groovy.transform.* + * + * {@code @ToString} + * {@code @Builder} + * class Person { + * String first, last + * int born + * + * Person(){} // required to retain no-arg constructor + * + * {@code @Builder}(builderClassName='MovieBuilder', builderMethodName='byRoleBuilder') + * Person(String roleName) { + * if (roleName == 'Jack Sparrow') { + * this.first = 'Johnny'; this.last = 'Depp'; this.born = 1963 + * } + * } + * + * {@code @Builder}(builderClassName='SplitBuilder', builderMethodName='splitBuilder') + * static Person split(String name, int year) { + * def parts = name.split(' ') + * new Person(first: parts[0], last: parts[1], born: year) + * } + * } + * + * assert Person.splitBuilder().name("Johnny Depp").year(1963).build().toString() == 'Person(Johnny, Depp, 1963)' + * assert Person.byRoleBuilder().roleName("Jack Sparrow").build().toString() == 'Person(Johnny, Depp, 1963)' + * assert Person.builder().first("Johnny").last('Depp').born(1963).build().toString() == 'Person(Johnny, Depp, 1963)' + * </pre> + * + * The 'forClass' annotation attribute for the {@code @Builder} transform isn't applicable for this strategy. + * The 'useSetters' annotation attribute for the {@code @Builder} transform is ignored by this strategy which always uses setters. + */ +public class DefaultStrategy extends BuilderASTTransformation.AbstractBuilderStrategy { + private static final Expression DEFAULT_INITIAL_VALUE = null; + private static final int PUBLIC_STATIC = ACC_PUBLIC | ACC_STATIC; + + public void build(BuilderASTTransformation transform, AnnotatedNode annotatedNode, AnnotationNode anno) { + if (unsupportedAttribute(transform, anno, "forClass")) return; + if (annotatedNode instanceof ClassNode) { + buildClass(transform, (ClassNode) annotatedNode, anno); + } else if (annotatedNode instanceof MethodNode) { + buildMethod(transform, (MethodNode) annotatedNode, anno); + } + } + + public void buildMethod(BuilderASTTransformation transform, MethodNode mNode, AnnotationNode anno) { + if (transform.getMemberValue(anno, "includes") != null || transform.getMemberValue(anno, "excludes") != null) { + transform.addError("Error during " + BuilderASTTransformation.MY_TYPE_NAME + + " processing: includes/excludes only allowed on classes", anno); + } + ClassNode buildee = mNode.getDeclaringClass(); + ClassNode builder = createBuilder(anno, buildee); + createBuilderFactoryMethod(anno, buildee, builder); + for (Parameter parameter : mNode.getParameters()) { + builder.addField(createFieldCopy(buildee, parameter)); + builder.addMethod(createBuilderMethodForProp(builder, new PropertyInfo(parameter.getName(), parameter.getType()), getPrefix(anno))); + } + builder.addMethod(createBuildMethodForMethod(anno, buildee, mNode, mNode.getParameters())); + } + + public void buildClass(BuilderASTTransformation transform, ClassNode buildee, AnnotationNode anno) { + List<String> excludes = new ArrayList<String>(); + List<String> includes = new ArrayList<String>(); + includes.add(Undefined.STRING); + if (!getIncludeExclude(transform, anno, buildee, excludes, includes)) return; + if (includes.size() == 1 && Undefined.isUndefined(includes.get(0))) includes = null; + ClassNode builder = createBuilder(anno, buildee); + createBuilderFactoryMethod(anno, buildee, builder); +// List<FieldNode> fields = getFields(transform, anno, buildee); + boolean allNames = transform.memberHasValue(anno, "allNames", true); + boolean allProperties = !transform.memberHasValue(anno, "allProperties", false); + List<PropertyInfo> props = getPropertyInfos(transform, anno, buildee, excludes, includes, allNames, allProperties); + for (PropertyInfo pi : props) { + ClassNode correctedType = getCorrectedType(buildee, pi.getType(), builder); + String fieldName = pi.getName(); + builder.addField(createFieldCopy(buildee, fieldName, correctedType)); + builder.addMethod(createBuilderMethodForProp(builder, new PropertyInfo(fieldName, correctedType), getPrefix(anno))); + } + builder.addMethod(createBuildMethod(anno, buildee, props)); + } + + private static ClassNode getCorrectedType(ClassNode buildee, ClassNode fieldType, ClassNode declaringClass) { + Map<String,ClassNode> genericsSpec = createGenericsSpec(declaringClass); + extractSuperClassGenerics(fieldType, buildee, genericsSpec); + return correctToGenericsSpecRecurse(genericsSpec, fieldType); + } + + private static void createBuilderFactoryMethod(AnnotationNode anno, ClassNode buildee, ClassNode builder) { + buildee.getModule().addClass(builder); + buildee.addMethod(createBuilderMethod(anno, builder)); + } + + private static ClassNode createBuilder(AnnotationNode anno, ClassNode buildee) { + return new InnerClassNode(buildee, getFullName(anno, buildee), PUBLIC_STATIC, OBJECT_TYPE); + } + + private static String getFullName(AnnotationNode anno, ClassNode buildee) { + String builderClassName = getMemberStringValue(anno, "builderClassName", buildee.getNameWithoutPackage() + "Builder"); + return buildee.getName() + "$" + builderClassName; + } + + private static String getPrefix(AnnotationNode anno) { + return getMemberStringValue(anno, "prefix", ""); + } + + private static MethodNode createBuildMethodForMethod(AnnotationNode anno, ClassNode buildee, MethodNode mNode, Parameter[] params) { + String buildMethodName = getMemberStringValue(anno, "buildMethodName", "build"); + final BlockStatement body = new BlockStatement(); + ClassNode returnType; + if (mNode instanceof ConstructorNode) { + returnType = newClass(buildee); + body.addStatement(returnS(ctorX(newClass(mNode.getDeclaringClass()), args(params)))); + } else { + body.addStatement(returnS(callX(newClass(mNode.getDeclaringClass()), mNode.getName(), args(params)))); + returnType = newClass(mNode.getReturnType()); + } + return new MethodNode(buildMethodName, ACC_PUBLIC, returnType, NO_PARAMS, NO_EXCEPTIONS, body); + } + + private static MethodNode createBuilderMethod(AnnotationNode anno, ClassNode builder) { + String builderMethodName = getMemberStringValue(anno, "builderMethodName", "builder"); + final BlockStatement body = new BlockStatement(); + body.addStatement(returnS(ctorX(builder))); + return new MethodNode(builderMethodName, PUBLIC_STATIC, builder, NO_PARAMS, NO_EXCEPTIONS, body); + } + + private static MethodNode createBuildMethod(AnnotationNode anno, ClassNode buildee, List<PropertyInfo> props) { + String buildMethodName = getMemberStringValue(anno, "buildMethodName", "build"); + final BlockStatement body = new BlockStatement(); + body.addStatement(returnS(initializeInstance(buildee, props, body))); + return new MethodNode(buildMethodName, ACC_PUBLIC, newClass(buildee), NO_PARAMS, NO_EXCEPTIONS, body); + } + + private MethodNode createBuilderMethodForProp(ClassNode builder, PropertyInfo pinfo, String prefix) { + ClassNode fieldType = pinfo.getType(); + String fieldName = pinfo.getName(); + String setterName = getSetterName(prefix, fieldName); + return new MethodNode(setterName, ACC_PUBLIC, newClass(builder), params(param(fieldType, fieldName)), NO_EXCEPTIONS, block( + stmt(assignX(propX(varX("this"), constX(fieldName)), varX(fieldName, fieldType))), + returnS(varX("this", builder)) + )); + } + + private static FieldNode createFieldCopy(ClassNode buildee, Parameter param) { + Map<String,ClassNode> genericsSpec = createGenericsSpec(buildee); + extractSuperClassGenerics(param.getType(), buildee, genericsSpec); + ClassNode correctedParamType = correctToGenericsSpecRecurse(genericsSpec, param.getType()); + return new FieldNode(param.getName(), ACC_PRIVATE, correctedParamType, buildee, param.getInitialExpression()); + } + + private static FieldNode createFieldCopy(ClassNode buildee, String fieldName, ClassNode fieldType) { + return new FieldNode(fieldName, ACC_PRIVATE, fieldType, buildee, DEFAULT_INITIAL_VALUE); + } + + private static Expression initializeInstance(ClassNode buildee, List<PropertyInfo> props, BlockStatement body) { + Expression instance = varX("_the" + buildee.getNameWithoutPackage(), buildee); + body.addStatement(declS(instance, ctorX(buildee))); + for (PropertyInfo pi : props) { + body.addStatement(stmt(assignX(propX(instance, pi.getName()), varX(pi.getName(), pi.getType())))); + } + return instance; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/d638ca43/src/main/groovy/groovy/transform/builder/ExternalStrategy.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/builder/ExternalStrategy.java b/src/main/groovy/groovy/transform/builder/ExternalStrategy.java new file mode 100644 index 0000000..c482bef --- /dev/null +++ b/src/main/groovy/groovy/transform/builder/ExternalStrategy.java @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.transform.builder; + +import groovy.transform.Undefined; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.transform.BuilderASTTransformation; + +import java.util.ArrayList; +import java.util.List; + +import static org.codehaus.groovy.ast.tools.GeneralUtils.assignX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.block; +import static org.codehaus.groovy.ast.tools.GeneralUtils.constX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.declS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.param; +import static org.codehaus.groovy.ast.tools.GeneralUtils.params; +import static org.codehaus.groovy.ast.tools.GeneralUtils.propX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt; +import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; +import static org.codehaus.groovy.ast.tools.GenericsUtils.newClass; +import static org.codehaus.groovy.transform.BuilderASTTransformation.MY_TYPE_NAME; +import static org.codehaus.groovy.transform.BuilderASTTransformation.NO_EXCEPTIONS; +import static org.codehaus.groovy.transform.BuilderASTTransformation.NO_PARAMS; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; + +/** + * This strategy is used with the {@link Builder} AST transform to populate a builder helper class + * so that it can be used for the fluent creation of instances of a specified class. The specified class is not modified in any way and may be a Java class. + * + * You use it by creating and annotating an explicit builder class which will be filled in by during + * annotation processing with the appropriate build method and setters. An example is shown here: + * <pre class="groovyTestCase"> + * import groovy.transform.builder.* + * + * class Person { + * String firstName + * String lastName + * } + * + * {@code @Builder}(builderStrategy=ExternalStrategy, forClass=Person) + * class PersonBuilder { } + * + * def person = new PersonBuilder().firstName("Robert").lastName("Lewandowski").build() + * assert person.firstName == "Robert" + * assert person.lastName == "Lewandowski" + * </pre> + * The {@code prefix} annotation attribute, which defaults to the empty String for this strategy, can be used to create setters with a different naming convention, e.g. with + * the {@code prefix} changed to 'set', you would use your setters as follows: + * <pre> + * def p1 = new PersonBuilder().setFirstName("Robert").setLastName("Lewandowski").setAge(21).build() + * </pre> + * or using a prefix of 'with': + * <pre> + * def p2 = new PersonBuilder().withFirstName("Robert").withLastName("Lewandowski").withAge(21).build() + * </pre> + * + * The properties to use can be filtered using either the 'includes' or 'excludes' annotation attributes for {@code @Builder}. + * The {@code @Builder} 'buildMethodName' annotation attribute can be used for configuring the build method's name, default "build". + * + * The {@code @Builder} 'builderMethodName' and 'builderClassName' annotation attributes aren't applicable for this strategy. + * The {@code @Builder} 'useSetters' annotation attribute is ignored by this strategy which always uses setters. + */ +public class ExternalStrategy extends BuilderASTTransformation.AbstractBuilderStrategy { + private static final Expression DEFAULT_INITIAL_VALUE = null; + + public void build(BuilderASTTransformation transform, AnnotatedNode annotatedNode, AnnotationNode anno) { + if (!(annotatedNode instanceof ClassNode)) { + transform.addError("Error during " + BuilderASTTransformation.MY_TYPE_NAME + " processing: building for " + + annotatedNode.getClass().getSimpleName() + " not supported by " + getClass().getSimpleName(), annotatedNode); + return; + } + ClassNode builder = (ClassNode) annotatedNode; + String prefix = transform.getMemberStringValue(anno, "prefix", ""); + ClassNode buildee = transform.getMemberClassValue(anno, "forClass"); + if (buildee == null) { + transform.addError("Error during " + MY_TYPE_NAME + " processing: 'forClass' must be specified for " + getClass().getName(), anno); + return; + } + List<String> excludes = new ArrayList<String>(); + List<String> includes = new ArrayList<String>(); + includes.add(Undefined.STRING); + if (!getIncludeExclude(transform, anno, buildee, excludes, includes)) return; + if (includes.size() == 1 && Undefined.isUndefined(includes.get(0))) includes = null; + if (unsupportedAttribute(transform, anno, "builderClassName")) return; + if (unsupportedAttribute(transform, anno, "builderMethodName")) return; + boolean allNames = transform.memberHasValue(anno, "allNames", true); + boolean allProperties = !transform.memberHasValue(anno, "allProperties", false); + List<PropertyInfo> props = getPropertyInfos(transform, anno, buildee, excludes, includes, allNames, allProperties); + if (includes != null) { + for (String name : includes) { + checkKnownProperty(transform, anno, name, props); + } + } + for (PropertyInfo prop : props) { + builder.addField(createFieldCopy(builder, prop)); + builder.addMethod(createBuilderMethodForField(builder, prop, prefix)); + } + builder.addMethod(createBuildMethod(transform, anno, buildee, props)); + } + + private static MethodNode createBuildMethod(BuilderASTTransformation transform, AnnotationNode anno, ClassNode sourceClass, List<PropertyInfo> fields) { + String buildMethodName = transform.getMemberStringValue(anno, "buildMethodName", "build"); + final BlockStatement body = new BlockStatement(); + Expression sourceClassInstance = initializeInstance(sourceClass, fields, body); + body.addStatement(returnS(sourceClassInstance)); + return new MethodNode(buildMethodName, ACC_PUBLIC, sourceClass, NO_PARAMS, NO_EXCEPTIONS, body); + } + + private MethodNode createBuilderMethodForField(ClassNode builderClass, PropertyInfo prop, String prefix) { + String propName = prop.getName().equals("class") ? "clazz" : prop.getName(); + String setterName = getSetterName(prefix, prop.getName()); + return new MethodNode(setterName, ACC_PUBLIC, newClass(builderClass), params(param(newClass(prop.getType()), propName)), NO_EXCEPTIONS, block( + stmt(assignX(propX(varX("this"), constX(propName)), varX(propName))), + returnS(varX("this", newClass(builderClass))) + )); + } + + private static FieldNode createFieldCopy(ClassNode builderClass, PropertyInfo prop) { + String propName = prop.getName(); + return new FieldNode(propName.equals("class") ? "clazz" : propName, ACC_PRIVATE, newClass(prop.getType()), builderClass, DEFAULT_INITIAL_VALUE); + } + + private static Expression initializeInstance(ClassNode sourceClass, List<PropertyInfo> props, BlockStatement body) { + Expression instance = varX("_the" + sourceClass.getNameWithoutPackage(), sourceClass); + body.addStatement(declS(instance, ctorX(sourceClass))); + for (PropertyInfo prop : props) { + body.addStatement(stmt(assignX(propX(instance, prop.getName()), varX(prop.getName().equals("class") ? "clazz" : prop.getName(), newClass(prop.getType()))))); + } + return instance; + } + +}
