Repository: groovy Updated Branches: refs/heads/GROOVY_2_6_X eb559d9e8 -> 1acfd614d
GROOVY-8453: @TupleConstructor ignoring JavaBean properties (including inherited properties) (closes #657) Project: http://git-wip-us.apache.org/repos/asf/groovy/repo Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/1acfd614 Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/1acfd614 Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/1acfd614 Branch: refs/heads/GROOVY_2_6_X Commit: 1acfd614dc2367bc108aba25897bb0c101f0699f Parents: eb559d9 Author: paulk <[email protected]> Authored: Mon Jan 22 14:47:57 2018 +1000 Committer: paulk <[email protected]> Committed: Wed Jan 24 01:16:10 2018 +1000 ---------------------------------------------------------------------- .../groovy/transform/TupleConstructor.java | 13 ++- .../codehaus/groovy/ast/tools/BeanUtils.java | 110 ++++++++++++------- .../codehaus/groovy/ast/tools/GeneralUtils.java | 31 ++++-- .../TupleConstructorASTTransformation.java | 39 ++++--- .../TupleConstructorTransformTest.groovy | 27 ++++- 5 files changed, 152 insertions(+), 68 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/groovy/blob/1acfd614/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 index 2cd2be7..12e0cb6 100644 --- a/src/main/groovy/groovy/transform/TupleConstructor.java +++ b/src/main/groovy/groovy/transform/TupleConstructor.java @@ -46,7 +46,9 @@ import java.lang.annotation.Target; * 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). + * super properties). The default order is properties, pseudo/JavaBean properties and then fields + * for parent classes first (if includeSuperXxx annotation attributes are used). + * * 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. @@ -262,6 +264,15 @@ public @interface TupleConstructor { boolean allNames() default false; /** + * Whether to include all properties (as per the JavaBean spec) in the generated constructor. + * When true, Groovy treats any explicitly created setXxx() methods as property setters as per the JavaBean + * specification. + * + * @since 2.5.0 + */ + boolean allProperties() 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. * http://git-wip-us.apache.org/repos/asf/groovy/blob/1acfd614/src/main/java/org/codehaus/groovy/ast/tools/BeanUtils.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/ast/tools/BeanUtils.java b/src/main/java/org/codehaus/groovy/ast/tools/BeanUtils.java index 32d4a81..5869d4e 100644 --- a/src/main/java/org/codehaus/groovy/ast/tools/BeanUtils.java +++ b/src/main/java/org/codehaus/groovy/ast/tools/BeanUtils.java @@ -22,6 +22,7 @@ import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.PropertyNode; +import org.codehaus.groovy.ast.stmt.Statement; import java.util.ArrayList; import java.util.HashSet; @@ -32,6 +33,7 @@ import static java.beans.Introspector.decapitalize; public class BeanUtils { static final String GET_PREFIX = "get"; + static final String SET_PREFIX = "set"; static final String IS_PREFIX = "is"; /** @@ -44,17 +46,38 @@ public class BeanUtils { * @return the list of found property nodes */ public static List<PropertyNode> getAllProperties(ClassNode type, boolean includeSuperProperties, boolean includeStatic, boolean includePseudoGetters) { + return getAllProperties(type, includeSuperProperties, includeStatic, includePseudoGetters, false, false); + } + + /** + * Get all properties including JavaBean pseudo properties matching JavaBean getter or setter conventions. + * + * @param type the ClassNode + * @param includeSuperProperties whether to include super properties + * @param includeStatic whether to include static properties + * @param includePseudoGetters whether to include JavaBean pseudo (getXXX/isYYY) properties with no corresponding field + * @param includePseudoSetters whether to include JavaBean pseudo (setXXX) properties with no corresponding field + * @param superFirst are properties gathered first from parent classes + * @return the list of found property nodes + */ + public static List<PropertyNode> getAllProperties(ClassNode type, boolean includeSuperProperties, boolean includeStatic, boolean includePseudoGetters, boolean includePseudoSetters, boolean superFirst) { + return getAllProperties(type, type, new HashSet<String>(), includeSuperProperties, includeStatic, includePseudoGetters, includePseudoSetters, superFirst); + } + + private static List<PropertyNode> getAllProperties(ClassNode origType, ClassNode type, Set<String> names, boolean includeSuperProperties, boolean includeStatic, boolean includePseudoGetters, boolean includePseudoSetters, boolean superFirst) { // TODO add generics support so this can be used for @EAHC - // TODO add an includePseudoSetters so this can be used for @TupleConstructor - ClassNode node = type; + if (type == null) { + return new ArrayList<PropertyNode>(); + } List<PropertyNode> result = new ArrayList<PropertyNode>(); - Set<String> names = new HashSet<String>(); - while (node != null) { - addExplicitProperties(node, result, names, includeStatic); - if (!includeSuperProperties) break; - node = node.getSuperClass(); + if (superFirst && includeSuperProperties) { + result.addAll(getAllProperties(origType, type.getSuperClass(), names, includeSuperProperties, includeStatic, includePseudoGetters, includePseudoSetters, superFirst)); + } + addExplicitProperties(type, result, names, includeStatic); + addPseudoProperties(origType, type, result, names, includeStatic, includePseudoGetters, includePseudoSetters); + if (!superFirst && includeSuperProperties) { + result.addAll(getAllProperties(origType, type.getSuperClass(), names, includeSuperProperties, includeStatic, includePseudoGetters, includePseudoSetters, superFirst)); } - addPseudoProperties(type, result, names, includeStatic, includePseudoGetters, includeSuperProperties); return result; } @@ -69,20 +92,10 @@ public class BeanUtils { } } - private static void addPseudoProperties(ClassNode cNode, List<PropertyNode> result, Set<String> names, boolean includeStatic, boolean includePseudoGetters, boolean includeSuperProperties) { - if (!includePseudoGetters) return; + public static void addPseudoProperties(ClassNode origType, ClassNode cNode, List<PropertyNode> result, Set<String> names, boolean includeStatic, boolean includePseudoGetters, boolean includePseudoSetters) { + if (!includePseudoGetters && !includePseudoSetters) return; List<MethodNode> methods = cNode.getAllDeclaredMethods(); ClassNode node = cNode.getSuperClass(); - if (includeSuperProperties) { - while (node != null) { - for (MethodNode next : node.getAllDeclaredMethods()) { - if (!next.isPrivate()) { - methods.add(next); - } - } - node = node.getSuperClass(); - } - } for (MethodNode mNode : methods) { if (!includeStatic && mNode.isStatic()) continue; String name = mNode.getName(); @@ -90,31 +103,54 @@ public class BeanUtils { // Optimization: skip invalid propertyNames continue; } - if (mNode.getDeclaringClass() != cNode && mNode.isPrivate()) { + if (mNode.getDeclaringClass() != origType && mNode.isPrivate()) { // skip private super methods continue; } int paramCount = mNode.getParameters().length; - ClassNode returnType = mNode.getReturnType(); + ClassNode paramType = mNode.getReturnType(); + String propName = null; + Statement getter = null; + Statement setter = null; if (paramCount == 0) { - if (name.startsWith(GET_PREFIX)) { + if (includePseudoGetters && name.startsWith(GET_PREFIX)) { // Simple getter - String propName = decapitalize(name.substring(3)); - if (!names.contains(propName)) { - result.add(new PropertyNode(propName, mNode.getModifiers(), returnType, cNode, null, mNode.getCode(), null)); - names.add(propName); - } - } else { - if (name.startsWith(IS_PREFIX) && returnType.equals(ClassHelper.boolean_TYPE)) { - // boolean getter - String propName = decapitalize(name.substring(2)); - if (!names.contains(propName)) { - names.add(propName); - result.add(new PropertyNode(propName, mNode.getModifiers(), returnType, cNode, null, mNode.getCode(), null)); - } - } + propName = decapitalize(name.substring(3)); + getter = mNode.getCode(); + } else if (includePseudoGetters && name.startsWith(IS_PREFIX) && paramType.equals(ClassHelper.boolean_TYPE)) { + // boolean getter + propName = decapitalize(name.substring(2)); + getter = mNode.getCode(); + } + } else if (paramCount == 1) { + if (includePseudoSetters && name.startsWith(SET_PREFIX)) { + // Simple setter + propName = decapitalize(name.substring(3)); + setter = mNode.getCode(); + paramType = mNode.getParameters()[0].getType(); + + } + } + if (propName != null) { + addIfMissing(cNode, result, names, mNode, paramType, propName, getter, setter); + } + } + } + + private static void addIfMissing(ClassNode cNode, List<PropertyNode> result, Set<String> names, MethodNode mNode, ClassNode returnType, String propName, Statement getter, Statement setter) { + if (cNode.getProperty(propName) != null) return; + if (names.contains(propName)) { + for (PropertyNode pn : result) { + if (pn.getName().equals(propName) && getter != null && pn.getGetterBlock() == null) { + pn.setGetterBlock(getter); + } + if (pn.getName().equals(propName) && setter != null && pn.getSetterBlock() == null) { + pn.setSetterBlock(setter); } } + } else { + result.add(new PropertyNode(propName, mNode.getModifiers(), returnType, cNode, null, getter, setter)); + names.add(propName); } } } http://git-wip-us.apache.org/repos/asf/groovy/blob/1acfd614/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java index fa99ad4..b61ae64 100644 --- a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java +++ b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java @@ -466,24 +466,33 @@ public class GeneralUtils { return result; } - public static List<FieldNode> getAllFields(ClassNode cNode, boolean includeSuperProperties, boolean includeSuperFields) { - final List<FieldNode> result; - if (cNode == ClassHelper.OBJECT_TYPE) { - result = new ArrayList<FieldNode>(); + public static List<PropertyNode> getAllFields(Set<String> names, ClassNode cNode, boolean includeProperties, boolean includeFields, boolean allProperties, boolean traverseSuperClasses) { + return getAllFields(names, cNode, cNode, includeProperties, includeFields, allProperties, traverseSuperClasses); + } + + private static List<PropertyNode> getAllFields(Set<String> names, ClassNode origType, ClassNode cNode, boolean includeProperties, boolean includeFields, boolean allProperties, boolean traverseSuperClasses) { + final List<PropertyNode> result; + if (cNode == ClassHelper.OBJECT_TYPE || !traverseSuperClasses) { + result = new ArrayList<PropertyNode>(); } else { - result = getAllFields(cNode.getSuperClass(), includeSuperProperties, includeSuperFields); + result = getAllFields(names, origType, cNode.getSuperClass(), includeProperties, includeFields, allProperties, true); } - if (includeSuperProperties) { + if (includeProperties) { for (PropertyNode pNode : cNode.getProperties()) { - if (!pNode.isStatic()) { - result.add(pNode.getField()); + if (!pNode.isStatic() && !names.contains(pNode.getName())) { + result.add(pNode); + names.add(pNode.getName()); } } + if (allProperties) { + BeanUtils.addPseudoProperties(origType, cNode, result, names, false, false, true); + } } - if (includeSuperFields) { + if (includeFields) { for (FieldNode fNode : cNode.getFields()) { - if (!fNode.isStatic() && cNode.getProperty(fNode.getName()) == null) { - result.add(fNode); + if (!fNode.isStatic() && cNode.getProperty(fNode.getName()) == null && !names.contains(fNode.getName())) { + result.add(new PropertyNode(fNode, fNode.getModifiers(), null, null)); + names.add(fNode.getName()); } } } http://git-wip-us.apache.org/repos/asf/groovy/blob/1acfd614/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java index e469597..ce2e25d 100644 --- a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java @@ -43,9 +43,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import static org.codehaus.groovy.ast.ClassHelper.make; import static org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching; @@ -115,6 +117,7 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation boolean force = memberHasValue(anno, "force", true); boolean defaults = !memberHasValue(anno, "defaults", false); boolean useSetters = memberHasValue(anno, "useSetters", true); + boolean allProperties = memberHasValue(anno, "allProperties", true); List<String> excludes = getMemberStringList(anno, "excludes"); List<String> includes = getMemberStringList(anno, "includes"); boolean allNames = memberHasValue(anno, "allNames", true); @@ -134,7 +137,7 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation return; } createConstructor(this, cNode, includeFields, includeProperties, includeSuperFields, includeSuperProperties, - callSuper, force, excludes, includes, useSetters, defaults, allNames, sourceUnit, + callSuper, force, excludes, includes, useSetters, defaults, allNames, allProperties, sourceUnit, (ClosureExpression) pre, (ClosureExpression) post); if (pre != null) { anno.setMember("pre", new ClosureExpression(new Parameter[0], EmptyStatement.INSTANCE)); @@ -164,21 +167,27 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation List<String> excludes, final List<String> includes, boolean useSetters, boolean defaults, boolean allNames, SourceUnit sourceUnit, ClosureExpression pre, ClosureExpression post) { + createConstructor(xform, cNode, includeFields, includeProperties, includeSuperFields, includeSuperProperties, callSuper, force, excludes, includes, useSetters, defaults, allNames, false, sourceUnit, pre, post); + } + + public static void createConstructor(AbstractASTTransformation xform, ClassNode cNode, boolean includeFields, + boolean includeProperties, boolean includeSuperFields, boolean + includeSuperProperties, boolean callSuper, boolean force, + List<String> excludes, final List<String> includes, boolean useSetters, boolean + defaults, boolean allNames, boolean allProperties, SourceUnit sourceUnit, ClosureExpression + pre, ClosureExpression post) { // no processing if existing constructors found if (!cNode.getDeclaredConstructors().isEmpty() && !force) return; - List<FieldNode> superList = new ArrayList<FieldNode>(); + Set<String> names = new HashSet<String>(); + List<PropertyNode> superList; if (includeSuperProperties || includeSuperFields) { - superList.addAll(getAllFields(cNode.getSuperClass(), includeSuperProperties, includeSuperFields)); + superList = getAllFields(names, cNode.getSuperClass(), includeSuperProperties, includeSuperFields, allProperties, true); + } else { + superList = new ArrayList<PropertyNode>(); } - List<FieldNode> list = new ArrayList<FieldNode>(); - if (includeProperties) { - list.addAll(getInstancePropertyFields(cNode)); - } - if (includeFields) { - list.addAll(getInstanceNonPropertyFields(cNode)); - } + List<PropertyNode> list = getAllFields(names, cNode, true, includeFields, allProperties, false); final List<Parameter> params = new ArrayList<Parameter>(); final List<Expression> superParams = new ArrayList<Expression>(); @@ -192,8 +201,9 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation } } final BlockStatement body = new BlockStatement(); - for (FieldNode fNode : superList) { - String name = fNode.getName(); + for (PropertyNode pNode : superList) { + FieldNode fNode = pNode.getField(); + String name = pNode.getName(); if (shouldSkipUndefinedAware(name, excludes, includes, allNames)) continue; params.add(createParam(fNode, name, defaults, xform)); boolean hasSetter = cNode.getProperty(name) != null && !fNode.isFinal(); @@ -213,8 +223,9 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation if (!preBody.isEmpty()) { body.addStatements(preBody.getStatements()); } - for (FieldNode fNode : list) { - String name = fNode.getName(); + for (PropertyNode pNode : list) { + String name = pNode.getName(); + FieldNode fNode = pNode.getField(); if (shouldSkipUndefinedAware(name, excludes, includes, allNames)) continue; Parameter nextParam = createParam(fNode, name, defaults, xform); params.add(nextParam); http://git-wip-us.apache.org/repos/asf/groovy/blob/1acfd614/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy ---------------------------------------------------------------------- diff --git a/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy b/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy index 933b592..991cdb4 100644 --- a/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy +++ b/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy @@ -214,37 +214,54 @@ class TupleConstructorTransformTest extends GroovyShellTestCase { ''' } - void testSuperPropertyAndSuperFieldOrder_groovy8455() { + // GROOVY-8455, GROOVY-8453 + void testPropPsuedoPropAndFieldOrderIncludingInheritedMembers() { assertScript ''' - import groovy.transform.* + import groovy.transform.TupleConstructor + + class Basepubf{} + class Basep{} + class Basepp{} + class Base { + Basep basep + public Basepubf basepubf + protected Byte baseProtField + void setBasePseudoProp(Basepp bpp) {} + } class Foopubf{} class Foop{} - class Foo { + class Foopp{} + class Foo extends Base { Foop foop public Foopubf foopubf protected Short fooProtField + Foopp getFooPseudoProp() { null } } class Barpubf{} class Barp{} + class Barpp{} class Bar extends Foo { Barp barp public Barpubf barpubf protected Integer barProtField + void setBarPseudoProp(Barpp bpp) { } } class Bazpubf{} class Bazp{} - @TupleConstructor(includeSuperProperties=true, includeFields=true, includeSuperFields=true) + class Bazpp{} + @TupleConstructor(includeSuperProperties=true, includeFields=true, includeSuperFields=true, allProperties=true) class Baz extends Bar { Bazp bazp public Bazpubf bazpubf protected Long bazProtField + void setBazPseudoProp(Bazpp bpp) { } } assert Baz.constructors.max{ it.parameterTypes.size() }.toString() == - 'public Baz(Foop,Foopubf,java.lang.Short,Barp,Barpubf,java.lang.Integer,Bazp,Bazpubf,java.lang.Long)' + 'public Baz(Basep,Basepp,Basepubf,java.lang.Byte,Foop,Foopubf,java.lang.Short,Barp,Barpp,Barpubf,java.lang.Integer,Bazp,Bazpp,Bazpubf,java.lang.Long)' ''' }
