blob: 255a895b11a37c697c5817d0a57513a946c19064 [file] [log] [blame]
/*
* 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 org.codehaus.groovy.transform;
import groovy.lang.GroovyClassLoader;
import groovy.transform.CompilationUnitAware;
import groovy.transform.builder.Builder;
import groovy.transform.builder.DefaultStrategy;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ConstructorNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import java.util.ArrayList;
import java.util.List;
import static groovy.transform.Undefined.isUndefined;
import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstancePropertyFields;
import static org.codehaus.groovy.ast.tools.GeneralUtils.getSuperPropertyFields;
/**
* Handles generation of code for the {@link Builder} annotation.
*/
@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
public class BuilderASTTransformation extends AbstractASTTransformation implements CompilationUnitAware {
private static final Class MY_CLASS = Builder.class;
private static final ClassNode MY_TYPE = ClassHelper.make(MY_CLASS);
public static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
public static final ClassNode[] NO_EXCEPTIONS = ClassNode.EMPTY_ARRAY;
public static final Parameter[] NO_PARAMS = Parameter.EMPTY_ARRAY;
private CompilationUnit compilationUnit;
public void visit(ASTNode[] nodes, SourceUnit source) {
init(nodes, source);
AnnotatedNode parent = (AnnotatedNode) nodes[1];
AnnotationNode anno = (AnnotationNode) nodes[0];
if (!MY_TYPE.equals(anno.getClassNode())) return;
if (parent instanceof ClassNode || parent instanceof MethodNode) {
if (parent instanceof ClassNode && !checkNotInterface((ClassNode) parent, MY_TYPE_NAME)) return;
if (parent instanceof MethodNode && !checkStatic((MethodNode) parent, MY_TYPE_NAME)) return;
final GroovyClassLoader classLoader = compilationUnit != null ? compilationUnit.getTransformLoader() : source.getClassLoader();
final BuilderStrategy strategy = createBuilderStrategy(anno, classLoader);
if (strategy == null) return;
strategy.build(this, parent, anno);
}
}
public interface BuilderStrategy {
void build(BuilderASTTransformation transform, AnnotatedNode annotatedNode, AnnotationNode anno);
}
public abstract static class AbstractBuilderStrategy implements BuilderStrategy {
protected static List<PropertyInfo> getPropertyInfoFromClassNode(ClassNode cNode, List<String> includes, List<String> excludes) {
return getPropertyInfoFromClassNode(cNode, includes, excludes, false);
}
protected static List<PropertyInfo> getPropertyInfoFromClassNode(ClassNode cNode, List<String> includes, List<String> excludes, boolean allNames) {
List<PropertyInfo> props = new ArrayList<PropertyInfo>();
for (FieldNode fNode : getInstancePropertyFields(cNode)) {
if (shouldSkip(fNode.getName(), excludes, includes, allNames)) continue;
props.add(new PropertyInfo(fNode.getName(), fNode.getType()));
}
return props;
}
protected String getSetterName(String prefix, String fieldName) {
return prefix.isEmpty() ? fieldName : prefix + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
}
protected boolean unsupportedAttribute(BuilderASTTransformation transform, AnnotationNode anno, String memberName) {
return unsupportedAttribute(transform, anno, memberName, "");
}
protected boolean unsupportedAttribute(BuilderASTTransformation transform, AnnotationNode anno, String memberName, String extraMessage) {
Object memberValue = transform.getMemberValue(anno, memberName);
if (memberValue != null && memberValue instanceof String && isUndefined((String) memberValue)) return false;
if (memberValue == null) {
memberValue = transform.getMemberClassValue(anno, memberName);
if (memberValue != null && isUndefined((ClassNode) memberValue)) {
memberValue = null;
}
}
if (memberValue != null) {
String message = extraMessage.length() == 0 ? "" : " " + extraMessage;
transform.addError("Error during " + MY_TYPE_NAME + " processing: Annotation attribute '" + memberName + "' not supported by " + getClass().getName() + message, anno);
return true;
}
return false;
}
protected void checkKnownProperty(BuilderASTTransformation transform, AnnotationNode anno, String name, List<PropertyInfo> properties) {
for (PropertyInfo prop: properties) {
if (name.equals(prop.getName())) {
return;
}
}
transform.addError("Error during " + MY_TYPE_NAME + " processing: tried to include unknown property '" + name + "'", anno);
}
protected void checkKnownField(BuilderASTTransformation transform, AnnotationNode anno, String name, List<FieldNode> fields) {
for (FieldNode field: fields) {
if (name.equals(field.getName())) {
return;
}
}
transform.addError("Error during " + MY_TYPE_NAME + " processing: tried to include unknown property '" + name + "'", anno);
}
protected boolean getIncludeExclude(BuilderASTTransformation transform, AnnotationNode anno, ClassNode cNode, List<String> excludes, List<String> includes) {
List<String> directExcludes = transform.getMemberStringList(anno, "excludes");
if (directExcludes != null) excludes.addAll(directExcludes);
List<String> directIncludes = transform.getMemberStringList(anno, "includes");
if (directIncludes != null) {
includes.clear();
includes.addAll(directIncludes);
}
if (directIncludes == null && excludes.isEmpty()) {
if (transform.hasAnnotation(cNode, TupleConstructorASTTransformation.MY_TYPE)) {
AnnotationNode tupleConstructor = cNode.getAnnotations(TupleConstructorASTTransformation.MY_TYPE).get(0);
if (excludes.isEmpty()) {
List<String> tupleExcludes = transform.getMemberStringList(tupleConstructor, "excludes");
if (tupleExcludes != null) excludes.addAll(tupleExcludes);
}
if (includes.isEmpty()) {
List<String> tupleIncludes = transform.getMemberStringList(tupleConstructor, "includes");
if (tupleIncludes != null) {
includes.clear();
includes.addAll(tupleIncludes);
}
}
}
}
List<String> includesToCheck = includes.size() == 1 && isUndefined(includes.get(0)) ? null : includes;
return transform.checkIncludeExcludeUndefinedAware(anno, excludes, includesToCheck, MY_TYPE_NAME);
}
protected List<FieldNode> getFields(BuilderASTTransformation transform, AnnotationNode anno, ClassNode buildee) {
boolean includeSuperProperties = transform.memberHasValue(anno, "includeSuperProperties", true);
return includeSuperProperties ? getSuperPropertyFields(buildee) : getInstancePropertyFields(buildee);
}
protected static class PropertyInfo {
public PropertyInfo(String name, ClassNode type) {
this.name = name;
this.type = type;
}
private String name;
private ClassNode type;
public String getName() {
return name;
}
public ClassNode getType() {
return type;
}
public void setName(String name) {
this.name = name;
}
public void setType(ClassNode type) {
this.type = type;
}
}
}
private boolean checkStatic(MethodNode mNode, String annotationName) {
if (!mNode.isStatic() && !mNode.isStaticConstructor() && !(mNode instanceof ConstructorNode)) {
addError("Error processing method '" + mNode.getName() + "'. " +
annotationName + " not allowed for instance methods.", mNode);
return false;
}
return true;
}
private BuilderStrategy createBuilderStrategy(AnnotationNode anno, GroovyClassLoader loader) {
ClassNode strategyClass = getMemberClassValue(anno, "builderStrategy", ClassHelper.make(DefaultStrategy.class));
if (strategyClass == null) {
addError("Couldn't determine builderStrategy class", anno);
return null;
}
String className = strategyClass.getName();
try {
Object instance = loader.loadClass(className).newInstance();
if (instance == null) {
addError("Can't load builderStrategy '" + className + "'", anno);
return null;
}
if (!BuilderStrategy.class.isAssignableFrom(instance.getClass())) {
addError("The builderStrategy class '" + strategyClass.getName() + "' on " + MY_TYPE_NAME + " is not a builderStrategy", anno);
return null;
}
return (BuilderStrategy) instance;
} catch (Exception e) {
addError("Can't load builderStrategy '" + className + "' " + e, anno);
return null;
}
}
public void setCompilationUnit(final CompilationUnit unit) {
this.compilationUnit = unit;
}
}