All Downloads are FREE. Search and download functionalities are using the official Maven repository.

groovy.transform.builder.ExternalStrategy Maven / Gradle / Ivy

The newest version!
/*
 *  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 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_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:
 * 
 * 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"
 * 
* 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: *
 * def p1 = new PersonBuilder().setFirstName("Robert").setLastName("Lewandowski").setAge(21).build()
 * 
* or using a prefix of 'with': *
 * def p2 = new PersonBuilder().withFirstName("Robert").withLastName("Lewandowski").withAge(21).build()
 * 
* * 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 excludes = new ArrayList(); List includes = new ArrayList(); if (!getIncludeExclude(transform, anno, buildee, excludes, includes)) return; if (unsupportedAttribute(transform, anno, "builderClassName")) return; if (unsupportedAttribute(transform, anno, "builderMethodName")) return; List 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 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 getPropertyInfoFromBeanInfo(ClassNode cNode, List includes, List excludes) { final List result = new ArrayList(); 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 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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy