forked from groovy/groovy-core
/
ExternalStrategy.java
177 lines (165 loc) · 8.97 KB
/
ExternalStrategy.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
/*
* Copyright 2003-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package groovy.transform.builder;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.transform.AbstractASTTransformation;
import org.codehaus.groovy.transform.BuilderASTTransformation;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
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_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
/**
* 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>
* 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.
*
* @author Marcin Grzejszczak
* @author Paul King
*/
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>();
if (!getIncludeExclude(transform, anno, buildee, excludes, includes)) return;
if (unsupportedAttribute(transform, anno, "builderClassName")) return;
if (unsupportedAttribute(transform, anno, "builderMethodName")) return;
List<PropertyInfo> props;
if (buildee.getModule() == null) {
props = getPropertyInfoFromBeanInfo(buildee, includes, excludes);
} else {
props = getPropertyInfoFromClassNode(buildee, includes, excludes);
}
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);
}
public static List<PropertyInfo> getPropertyInfoFromBeanInfo(ClassNode cNode, List<String> includes, List<String> excludes) {
final List<PropertyInfo> result = new ArrayList<PropertyInfo>();
try {
BeanInfo beanInfo = Introspector.getBeanInfo(cNode.getTypeClass());
for (PropertyDescriptor descriptor : beanInfo.getPropertyDescriptors()) {
if (AbstractASTTransformation.shouldSkip(descriptor.getName(), excludes, includes)) continue;
// skip hidden and read-only props
if (descriptor.isHidden() || descriptor.getWriteMethod() == null) continue;
result.add(new PropertyInfo(descriptor.getName(), ClassHelper.make(descriptor.getPropertyType())));
}
} catch (IntrospectionException ignore) {
}
return result;
}
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;
}
}