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

org.joda.beans.gen.BeanGen Maven / Gradle / Ivy

The newest version!
/*
 *  Copyright 2001-present Stephen Colebourne
 *
 *  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 org.joda.beans.gen;

import static java.util.stream.Collectors.toList;

import java.io.File;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.BiConsumer;
import java.util.function.Function;

import org.joda.beans.Bean;
import org.joda.beans.BeanBuilder;
import org.joda.beans.JodaBeanUtils;
import org.joda.beans.MetaBean;
import org.joda.beans.MetaProperty;
import org.joda.beans.TypedMetaBean;
import org.joda.beans.impl.BasicBeanBuilder;
import org.joda.beans.impl.direct.DirectBean;
import org.joda.beans.impl.direct.DirectBeanBuilder;
import org.joda.beans.impl.direct.DirectFieldsBeanBuilder;
import org.joda.beans.impl.direct.DirectMetaBean;
import org.joda.beans.impl.direct.DirectMetaPropertyMap;
import org.joda.beans.impl.direct.DirectPrivateBeanBuilder;
import org.joda.beans.impl.direct.MinimalMetaBean;
import org.joda.beans.impl.light.LightMetaBean;

/**
 * Code generator for a bean.
 */
class BeanGen {

    /** Constructor style for none. */
    static final int CONSTRUCTOR_NONE = 0;
    /** Constructor style for builder-based. */
    static final int CONSTRUCTOR_BY_BUILDER = 1;
    /** Constructor style for argument-based. */
    static final int CONSTRUCTOR_BY_ARGS = 2;
    /** Class constant, avoiding module dependency in Java 9. */
    private static final Class CLASS_CONSTRUCTOR_PROPERTIES;
    /** Class constant, avoiding module dependency in Java 9. */
    private static final Class CLASS_PROPERTY_CHANGE_SUPPORT;
    static {
        Class cls1 = null;
        Class cls2 = null;
        try {
            cls1 = Class.forName("java.beans.ConstructorProperties");
            cls2 = Class.forName("java.beans.PropertyChangeSupport");
        } catch (ClassNotFoundException ex) {
            // ignore
        }
        CLASS_CONSTRUCTOR_PROPERTIES = cls1;
        CLASS_PROPERTY_CHANGE_SUPPORT = cls2;
    }
    /** Line separator. */
    private static final String LINE_SEPARATOR = "\t//-----------------------------------------------------------------------";
    /** Line separator. */
    private static final String LINE_SEPARATOR_INDENTED = "\t\t//-----------------------------------------------------------------------";
    /** Types with primitive equals. */
    private static final Set PRIMITIVE_EQUALS = new HashSet<>();
    static {
        PRIMITIVE_EQUALS.add("boolean");
        PRIMITIVE_EQUALS.add("char");
        PRIMITIVE_EQUALS.add("byte");
        PRIMITIVE_EQUALS.add("short");
        PRIMITIVE_EQUALS.add("int");
        PRIMITIVE_EQUALS.add("long");
        // not float or double, as Double.equals is not the same as double ==
    }

    /** The content to process. */
    private final File file;
    /** The content to process. */
    private final List content;
    /** The config. */
    private final BeanGenConfig config;
    /** The data model of the bean. */
    private final BeanData data;
    /** The list of property generators. */
    private final List properties;
    /** The region to insert into. */
    private final List insertRegion;
    /** The list of removed imports. */
    private final SortedSet removedImports = new TreeSet<>();

    /**
     * Constructor used when file is not a bean.
     * @param file  the file, not null
     * @param content  the content to process, not null
     * @param config  the config to use, not null
     * @param data  the parsed data
     */
    BeanGen(File file, List content, BeanGenConfig config, BeanData data) {
        this.file = file;
        this.content = content;
        this.config = config;
        this.data = data;
        this.properties = null;
        this.insertRegion = null;
    }

    /**
     * Constructor used when file is a parsed bean.
     * @param file  the file, not null
     * @param content  the content to process, not null
     * @param config  the config to use, not null
     * @param data  the parsed data
     * @param properties  the parsed properties
     * @param autoEndIndex  the start of the autogen area
     * @param autoStartIndex   the end of the autogen area
     */
    BeanGen(
            File file, List content, BeanGenConfig config,
            BeanData data, List properties, int autoStartIndex, int autoEndIndex) {
        this.file = file;
        this.content = content;
        this.config = config;
        this.data = data;
        this.properties = properties;
        this.insertRegion = content.subList(autoStartIndex + 1, autoEndIndex);
    }

    //-----------------------------------------------------------------------
    void process() {
        fixImports();
        if (insertRegion != null) {
            data.ensureImport(BeanDefinition.class);
            if (properties.size() > 0) {
                data.ensureImport(PropertyDefinition.class);
            }
            removeOld();
            if (data.isRootClass() && data.isExtendsDirectBean()) {
                data.ensureImport(DirectBean.class);
            }
            generateMeta();
            generateSerializationVersionId();
            generatePropertyChangeSupportField();
            generateHashCodeField();
            generateFactory();
            generateImmutableBuilderMethod();
            generateArgBasedConstructor();
            generateBuilderBasedConstructor();
            generateMetaBean();
            generateGettersSetters();
            generateSeparator();
            generateImmutableToBuilder();
            generateClone();
            generateEquals();
            generateHashCode();
            generateToString();
            generateMetaClass();
            generateBuilderClass();
            resolveImports();
            resolveIndents();
        }
    }

    void processNonBean() {
        fixImports();
        resolveImports();
    }

    private void fixImports() {
        renameImport("org.joda.beans.BeanDefinition", BeanDefinition.class);
        renameImport("org.joda.beans.DerivedProperty", DerivedProperty.class);
        renameImport("org.joda.beans.ImmutableConstructor", ImmutableConstructor.class);
        renameImport("org.joda.beans.ImmutableDefaults", ImmutableDefaults.class);
        renameImport("org.joda.beans.ImmutablePreBuild", ImmutablePreBuild.class);
        renameImport("org.joda.beans.ImmutableValidator", ImmutableValidator.class);
        renameImport("org.joda.beans.PropertyDefinition", PropertyDefinition.class);
    }

    private void renameImport(String old, Class cls) {
        if (data.getCurrentImports().contains(old)) {
            removedImports.add(old);
            data.ensureImport(cls);
        }
    }

    private void resolveImports() {
        if (data.getNewImports().size() > 0) {
            int pos = data.getImportInsertLocation() + 1;
            for (String imp : data.getNewImports()) {
                content.add(pos++, "import " + imp + ";");
            }
        }
        if (removedImports.size() > 0) {
            for (ListIterator it = content.listIterator(); it.hasNext(); ) {
                String line = it.next().trim();
                if (line.startsWith("import ")) {
                    String imported = line.substring(7).trim().replace(" ", "").replace(";", "");
                    if (removedImports.contains(imported)) {
                        it.remove();
                    }
                }
            }
        }
    }

    private void resolveIndents() {
        for (ListIterator it = content.listIterator(); it.hasNext(); ) {
            it.set(it.next().replace("\t", config.getIndent()));
        }
    }

    private void removeOld() {
        insertRegion.clear();
    }

    //-----------------------------------------------------------------------
    private void generateSeparator() {
        if (insertRegion.size() > 0 && insertRegion.get(insertRegion.size() - 1).equals(LINE_SEPARATOR)) {
            return;
        }
        addLine(0, LINE_SEPARATOR);
    }

    private void generateIndentedSeparator() {
        if (insertRegion.size() > 0 && insertRegion.get(insertRegion.size() - 1).equals(LINE_SEPARATOR_INDENTED)) {
            return;
        }
        addLine(0, LINE_SEPARATOR_INDENTED);
    }

    private void generateGenerated(int tabCount) {
        if (config.isGeneratedAnno()) {
            addLine(tabCount, "@Generated(\"org.joda.beans.gen.BeanCodeGen\")");
        }
    }

    private void generateFactory() {
        if (data.isFactoryRequired()) {
            List nonDerived = nonDerivedProperties();
            addLine(1, "/**");
            addLine(1, " * Obtains an instance.");
            if (nonDerived.size() > 0) {
                if (data.isTypeGeneric()) {
                    for (int j = 0; j < data.getTypeGenericCount(); j++) {
                        addLine(1, " * @param " + data.getTypeGenericName(j, true) + "  the type");
                    }
                }
                for (int i = 0; i < nonDerived.size(); i++) {
                    PropertyData prop = nonDerived.get(i).getData();
                    addLine(1, " * @param " + prop.getPropertyName() + "  the value of the property" + prop.getNotNullJavadoc());
                }
            }
            addLine(1, " * @return the instance");
            addLine(1, " */");
            if (nonDerived.isEmpty()) {
                addLine(1, "public static " + data.getTypeNoExtends() + " " + data.getFactoryName() + "() {");
                addLine(2, "return new " + data.getTypeNoExtends() + "();");
                
            } else {
                if (data.isTypeGeneric()) {
                    addLine(1, "public static " + data.getTypeGeneric(true) + " " +
                                    data.getTypeNoExtends() + " " + data.getFactoryName() + "(");
                } else {
                    addLine(1, "public static " + data.getTypeNoExtends() + " " + data.getFactoryName() + "(");
                }
                for (int i = 0; i < nonDerived.size(); i++) {
                    PropertyGen prop = nonDerived.get(i);
                    addLine(3, prop.getBuilderType() + " " + prop.getData().getPropertyName() + joinComma(i, nonDerived, ") {"));
                }
                addLine(2, "return new " + data.getTypeWithDiamond() + "(");
                for (int i = 0; i < nonDerived.size(); i++) {
                    addLine(3, nonDerived.get(i).generateBuilderFieldName() + joinComma(i, nonDerived, ");"));
                }
            }
            addLine(1, "}");
            addBlankLine();
        }
    }

    private void generateImmutableBuilderMethod() {
        if (data.isConstructable() && data.isBuilderGenerated()) {
            addLine(1, "/**");
            addLine(1, " * Returns a builder used to create an instance of the bean.");
            if (data.isTypeGeneric()) {
                for (int j = 0; j < data.getTypeGenericCount(); j++) {
                    addLine(1, " * @param " + data.getTypeGenericName(j, true) + "  the type");
                }
            }
            addLine(1, " * @return the builder, not null");
            addLine(1, " */");
            if (data.isTypeGeneric()) {
                addLine(1, data.getEffectiveBuilderScope() + "static " + data.getTypeGeneric(true) +
                                " " + data.getEffectiveBeanBuilderName() + data.getTypeGenericName(true) + " builder() {");
            } else {
                addLine(1, data.getEffectiveBuilderScope() + "static " + data.getEffectiveBeanBuilderName() + " builder() {");
            }
            addLine(2, "return new " + data.getEffectiveBeanBuilderName() + data.getTypeGenericDiamond() + "();");
            addLine(1, "}");
            addBlankLine();
        }
    }

    private void generateBuilderBasedConstructor() {
        if (data.getConstructorStyle() == CONSTRUCTOR_BY_BUILDER && data.getImmutableConstructor() == CONSTRUCTOR_NONE && 
                ((data.isMutable() && data.isBuilderScopeVisible()) || data.isImmutable())) {
            List nonDerived = nonDerivedProperties();
            String scope = (data.isTypeFinal() ? "private" : "protected");
            // signature
            addLine(1, "/**");
            addLine(1, " * Restricted constructor.");
            addLine(1, " * @param builder  the builder to copy from, not null");
            addLine(1, " */");
            addLine(1, scope + " " + data.getTypeRaw() + "(" + data.getEffectiveBeanBuilderName() + data.getTypeGenericName(true) + " builder) {");
            // super
            if (data.isSubClass()) {
                addLine(2, "super(builder);");
            }
            // validate
            for (PropertyGen prop : properties) {
                if (prop.getData().isValidated()) {
                    addLine(2, prop.getData().getValidationMethodName() +
                            "(builder." + prop.generateBuilderFieldName() +
                            ", \"" + prop.getData().getPropertyName() + "\");");
                }
            }
            // assign
            if (data.isImmutable()) {
                // assign
                for (int i = 0; i < nonDerived.size(); i++) {
                    addLines(nonDerived.get(i).generateConstructorAssign("builder."));
                }
            } else {
                for (int i = 0; i < nonDerived.size(); i++) {
                    PropertyGen propGen = nonDerived.get(i);
                    PropertyData prop = propGen.getData();
                    if (prop.isCollectionType()) {
                        if (prop.isNotNull()) {
                            addLine(2, "this." + prop.getPropertyName() + ".addAll(builder." + propGen.generateBuilderFieldName() + ");");
                        } else {
                            addLine(2, "this." + prop.getPropertyName() + " = builder." + propGen.generateBuilderFieldName() + ";");
                        }
                    } else if (prop.isMapType()) {
                        if (prop.isNotNull()) {
                            addLine(2, "this." + prop.getPropertyName() + ".putAll(builder." + propGen.generateBuilderFieldName() + ");");
                        } else {
                            addLine(2, "this." + prop.getPropertyName() + " = builder." + propGen.generateBuilderFieldName() + ";");
                        }
                    } else {
                        addLine(2, "this." + prop.getPropertyName() + " = builder." + propGen.generateBuilderFieldName() + ";");
                    }
                }
            }
            if (data.getImmutableValidator() != null) {
                addLine(2, data.getImmutableValidator() + "();");
            }
            addLine(1, "}");
            addBlankLine();
        }
    }

    private void generateArgBasedConstructor() {
        if (isArgBasedConstructor()) {
            String scope = data.getEffectiveConstructorScope();
            boolean generateAnnotation = data.isConstructorPropertiesAnnotation();
            boolean generateJavadoc = !"private ".equals(scope);
            List nonDerived = nonDerivedProperties();
            if (nonDerived.size() == 0) {
                if (generateJavadoc) {
                    addLine(1, "/**");
                    addLine(1, " * Creates an instance.");
                    addLine(1, " */");
                }
                if (generateAnnotation) {
                    data.ensureImport(CLASS_CONSTRUCTOR_PROPERTIES);
                    addLine(1, "@ConstructorProperties({})");
                }
                addLine(1, scope + data.getTypeRaw() + "() {");
            } else {
                // signature
                if (generateJavadoc) {
                    addLine(1, "/**");
                    addLine(1, " * Creates an instance.");
                    for (int i = 0; i < nonDerived.size(); i++) {
                        PropertyData prop = nonDerived.get(i).getData();
                        addLine(1, " * @param " + prop.getPropertyName() + "  the value of the property" + prop.getNotNullJavadoc());
                    }
                    addLine(1, " */");
                }
                if (generateAnnotation) {
                    data.ensureImport(CLASS_CONSTRUCTOR_PROPERTIES);
                    StringBuilder buf = new StringBuilder();
                    for (int i = 0; i < nonDerived.size(); i++) {
                        buf.append('"').append(nonDerived.get(i).getData().getPropertyName()).append('"');
                        buf.append(join(i, nonDerived, ", ", ""));
                    }
                    addLine(1, "@ConstructorProperties({" + buf.toString() + "})");
                }
                addLine(1, scope + data.getTypeRaw() + "(");
                for (int i = 0; i < nonDerived.size(); i++) {
                    PropertyGen prop = nonDerived.get(i);
                    addLine(3, prop.getBuilderType() + " " + prop.getData().getPropertyName() + joinComma(i, nonDerived, ") {"));
                }
                // validate (mutable light beans call setters which validate)
                if (!(data.isMutable() && data.isBeanStyleLight())) {
                    for (PropertyGen prop : properties) {
                        if (prop.getData().isValidated()) {
                            addLine(2, prop.getData().getValidationMethodName() +
                                    "(" + prop.getData().getPropertyName() +
                                    ", \"" + prop.getData().getPropertyName() + "\");");
                        }
                    }
                }
                // assign
                for (int i = 0; i < nonDerived.size(); i++) {
                    PropertyGen prop = nonDerived.get(i);
                    if (data.isMutable() && data.isBeanStyleLight()) {
                        String generateSetInvoke = prop.getData().getSetterGen().generateSetInvoke(
                                prop.getData(), prop.getData().getPropertyName());
                        addLine(2, generateSetInvoke + ";");
                    } else {
                        addLines(prop.generateConstructorAssign(""));
                    }
                }
            }
            if (data.getImmutableValidator() != null) {
                addLine(2, data.getImmutableValidator() + "();");
            }
            addLine(1, "}");
            addBlankLine();
        }
    }

    private boolean isArgBasedConstructor() {
        if (data.getConstructorStyle() == CONSTRUCTOR_BY_ARGS &&
                data.getImmutableConstructor() == CONSTRUCTOR_NONE &&
                ((data.isMutable() && (data.isBuilderScopeVisible() || data.isBeanStyleLight())) || data.isImmutable())) {
            return true;
        }
        if (data.getConstructorStyle() == CONSTRUCTOR_BY_BUILDER &&
                data.getImmutableConstructor() == CONSTRUCTOR_NONE &&
                data.isImmutable() &&
                !data.isTypeFinal() &&
                !"smart".equals(data.getConstructorScope())) {
            return true;
        }
        return false;
    }

    //-----------------------------------------------------------------------
    private void generateMeta() {
        if (data.isBeanStyleLightOrMinimal()) {
            addLine(1, "/**");
            addLine(1, " * The meta-bean for {@code " + data.getTypeRaw() + "}.");
            addLine(1, " */");
            boolean genericProps = data.getProperties().stream()
                    .filter(p -> p.isGeneric())
                    .findAny()
                    .isPresent();
            boolean unchecked = data.isBeanStyleMinimal() && data.isMutable() && genericProps;
            unchecked |= data.isBeanStyleMinimal() && data.isTypeGeneric() && !data.isSkipBuilderGeneration();
            boolean rawtypes = data.isBeanStyleMinimal() && data.isTypeGeneric();
            if (unchecked && rawtypes) {
                addLine(1, "@SuppressWarnings({\"unchecked\", \"rawtypes\" })");
            } else if (rawtypes) {
                addLine(1, "@SuppressWarnings(\"rawtypes\")");
            } else if (unchecked) {
                addLine(1, "@SuppressWarnings(\"unchecked\")");
            }
            if (data.isTypeGeneric()) {
                data.ensureImport(MetaBean.class);
                addLine(1, "private static final MetaBean META_BEAN =");
            } else {
                data.ensureImport(TypedMetaBean.class);
                addLine(1, "private static final TypedMetaBean<" + data.getTypeNoExtends() + "> META_BEAN =");
            }
            List nonDerived = nonDerivedProperties();
            List aliases = nonDerived.stream().filter(p -> p.getData().getAlias() != null).collect(toList());
            boolean hasAliases = aliases.isEmpty();
            if (data.isBeanStyleLight()) {
                // light
                data.ensureImport(LightMetaBean.class);
                data.ensureImport(MethodHandles.class);
                boolean specialInit = nonDerived.stream().filter(p -> p.isSpecialInit()).findAny().isPresent();
                if (nonDerived.isEmpty()) {
                    addLine(3, "LightMetaBean.of(" + data.getTypeRaw() + ".class, MethodHandles.lookup());");
                } else {
                    addLine(3, "LightMetaBean.of(");
                    addLine(5, data.getTypeRaw() + ".class,");
                    addLine(5, "MethodHandles.lookup(),");
                    generateFieldNames(nonDerived);
                    if (specialInit) {
                        for (int i = 0; i < nonDerived.size(); i++) {
                            addLine(5, nonDerived.get(i).generateInit() +
                                    joinComma(i, nonDerived, ")" + (hasAliases ? ";" : "")));
                        }
                    } else {
                        addLine(5, "new Object[0])" + (hasAliases ? ";" : ""));
                    }
                    for (int i = 0; i < aliases.size(); i++) {
                        PropertyGen prop = aliases.get(i);
                        addLine(5, ".withAlias(\"" + prop.getData().getAlias() + "\", \"" +
                                prop.getData().getPropertyName() + "\")" + join(i, aliases, "", ";"));
                    }
                }
            } else {
                // minimal
                data.ensureImport(MinimalMetaBean.class);
                addLine(3, "MinimalMetaBean.of(");
                addLine(5, data.getTypeRaw() + ".class,");
                generateFieldNames(nonDerived);
                String builderLambda = "() -> new " + data.getEffectiveBeanBuilderName() + "()";
                if (data.isSkipBuilderGeneration()) {
                    if (data.isBeanBuilderManual()) {
                        builderLambda = "() -> new " + data.getEffectiveMinimalBeanBuilderName() + "()";
                    } else {
                        data.ensureImport(BasicBeanBuilder.class);
                        builderLambda = "() -> new BasicBeanBuilder<>(new " + data.getTypeWithDiamond() + "())";
                    }
                }
                if (nonDerived.isEmpty()) {
                    if (data.isImmutable()) {
                        addLine(5, builderLambda + ");");
                    } else {
                        data.ensureImport(Collections.class);
                        data.ensureImport(Function.class);
                        data.ensureImport(BiConsumer.class);
                        addLine(5, builderLambda + ",");
                        addLine(5, "Collections.>emptyList(),");
                        addLine(5, "Collections.>emptyList());");
                    }
                } else {
                    addLine(5, builderLambda + ",");
                    if (data.isImmutable()) {
                        for (int i = 0; i < nonDerived.size(); i++) {
                            addLine(5, nonDerived.get(i).generateLambdaGetter() + joinComma(i, nonDerived, ")" +
                                    (hasAliases ? ";" : "")));
                        }
                    } else {
                        data.ensureImport(Arrays.class);
                        data.ensureImport(Function.class);
                        data.ensureImport(BiConsumer.class);
                        addLine(5, "Arrays.>asList(");
                        for (int i = 0; i < nonDerived.size(); i++) {
                            addLine(7, nonDerived.get(i).generateLambdaGetter() + joinComma(i, nonDerived, "),"));
                        }
                        addLine(5, "Arrays.>asList(");
                        for (int i = 0; i < nonDerived.size(); i++) {
                            addLine(7, nonDerived.get(i).generateLambdaSetter() +
                                    joinComma(i, nonDerived, "))" + (hasAliases ? ";" : "")));
                        }
                    }
                    for (int i = 0; i < aliases.size(); i++) {
                        PropertyGen prop = aliases.get(i);
                        addLine(5, ".withAlias(\"" + prop.getData().getAlias() + "\", \"" +
                                prop.getData().getPropertyName() + "\")" + join(i, aliases, "", ";"));
                    }
                }
            }
            addBlankLine();
            addLine(1, "/**");
            addLine(1, " * The meta-bean for {@code " + data.getTypeRaw() + "}.");
            addLine(1, " * @return the meta-bean, not null");
            addLine(1, " */");
            if (data.isTypeGeneric()) {
                addLine(1, "public static MetaBean meta() {");
            } else {
                addLine(1, "public static TypedMetaBean<" + data.getTypeNoExtends() + "> meta() {");
            }
            addLine(2, "return META_BEAN;");
            addLine(1, "}");
            addBlankLine();
            addLine(1, "static {");
            data.ensureImport(MetaBean.class);
            addLine(2, "MetaBean.register(META_BEAN);");
            addLine(1, "}");
            addBlankLine();
            
        } else {
            // this cannot be generified without either Eclipse or javac complaining
            // raw types forever
            addLine(1, "/**");
            addLine(1, " * The meta-bean for {@code " + data.getTypeRaw() + "}.");
            addLine(1, " * @return the meta-bean, not null");
            if (data.isMetaScopePrivate()) {
                data.ensureImport(MetaBean.class);
                addLine(1, " */");
                addLine(1, "public static MetaBean meta() {");
            } else if (data.isTypeGeneric()) {
                addLine(1, " */");
                addLine(1, "@SuppressWarnings(\"rawtypes\")");
                addLine(1, "public static " + data.getTypeRaw() + ".Meta meta() {");
            } else {
                addLine(1, " */");
                addLine(1, "public static " + data.getTypeRaw() + ".Meta meta() {");
            }
            addLine(2, "return " + data.getTypeRaw() + ".Meta.INSTANCE;");
            addLine(1, "}");
            
            if (data.isTypeGeneric()) {
                generateMetaForGenericType();
            }
            
            addBlankLine();
            addLine(1, "static {");
            data.ensureImport(MetaBean.class);
            addLine(2, "MetaBean.register(" + data.getTypeRaw() + ".Meta.INSTANCE);");
            addLine(1, "}");
            addBlankLine();
        }
    }

    private void generateFieldNames(List nonDerived) {
        if (nonDerived.isEmpty()) {
            addLine(5, "new String[0],");
        } else {
            addLine(5, "new String[] {");
            for (int i = 0; i < nonDerived.size(); i++) {
                addLine(7, "\"" + nonDerived.get(i).getData().getFieldName() + join(i, nonDerived, "\",", "\"},"));
            }
        }
    }

    private void generateMetaForGenericType() {
        // this works around an Eclipse bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=397462
        // long name needed for uniqueness as static overriding is borked
        addBlankLine();
        addLine(1, "/**");
        addLine(1, " * The meta-bean for {@code " + data.getTypeRaw() + "}.");
        if (data.getTypeGenericCount() == 1) {
            addLine(1, " * @param   the bean's generic type");
            addLine(1, " * @param cls  the bean's generic type");
        } else if (data.getTypeGenericCount() == 2) {
            addLine(1, " * @param   the first generic type");
            addLine(1, " * @param   the second generic type");
            addLine(1, " * @param cls1  the first generic type");
            addLine(1, " * @param cls2  the second generic type");
        } else if (data.getTypeGenericCount() == 3) {
            addLine(1, " * @param   the first generic type");
            addLine(1, " * @param   the second generic type");
            addLine(1, " * @param   the second generic type");
            addLine(1, " * @param cls1  the first generic type");
            addLine(1, " * @param cls2  the second generic type");
            addLine(1, " * @param cls3  the third generic type");
        }
        addLine(1, " * @return the meta-bean, not null");
        addLine(1, " */");
        addLine(1, "@SuppressWarnings(\"unchecked\")");
        String[] typeNames = new String[] {"R", "S", "T"};
        if (data.getTypeGenericCount() == 1) {
            addLine(1, "public static  " + data.getTypeRaw() + ".Meta meta" + data.getTypeRaw() + "(Class cls) {");
        } else if (data.getTypeGenericCount() == 2) {
            addLine(1, "public static  " + data.getTypeRaw() +
                    ".Meta meta" + data.getTypeRaw() + "(Class cls1, Class cls2) {");
        } else if (data.getTypeGenericCount() == 3) {
            addLine(1, "public static  " + data.getTypeRaw() +
                    ".Meta meta" + data.getTypeRaw() + "(Class cls1, Class cls2, Class cls3) {");
        }
        addLine(2, "return " + data.getTypeRaw() + ".Meta.INSTANCE;");
        addLine(1, "}");
    }

    private void generateSerializationVersionId() {
        if (data.isSerializable() && !data.isManualSerializationId()) {
            addLine(1, "/**");
            addLine(1, " * The serialization version id.");
            addLine(1, " */");
            addLine(1, "private static final long serialVersionUID = 1L;");
            addBlankLine();
        }
    }

    private void generatePropertyChangeSupportField() {
        if (data.isPropertyChangeSupport()) {
            data.ensureImport(CLASS_PROPERTY_CHANGE_SUPPORT);
            addLine(1, "/**");
            addLine(1, " * The property change support field.");
            addLine(1, " */");
            addLine(1, "private final transient PropertyChangeSupport " + config.getPrefix() + "propertyChangeSupport = new PropertyChangeSupport(this);");
            addBlankLine();
        }
    }

    private void generateHashCodeField() {
        if (data.isCacheHashCode()) {
            addLine(1, "/**");
            addLine(1, " * The cached hash code, using the racy single-check idiom.");
            addLine(1, " */");
            addLine(1, "private transient int " + config.getPrefix() + "cacheHashCode;");
            addBlankLine();
        }
    }

    private void generateMetaBean() {
        if (data.isMetaScopePrivate() || data.isBeanStyleMinimal()) {
            addLine(1, "@Override");
            if (data.isBeanStyleLightOrMinimal()) {
                data.ensureImport(TypedMetaBean.class);
                if (data.isTypeGeneric()) {
                    addLine(1, "@SuppressWarnings(\"unchecked\")");
                }
                addLine(1, "public TypedMetaBean<" + data.getTypeNoExtends() + "> metaBean() {");
                if (data.isTypeGeneric()) {
                    addLine(2, "return (TypedMetaBean<" + data.getTypeNoExtends() + ">) META_BEAN;");
                } else {
                    addLine(2, "return META_BEAN;");
                }
            } else {
                data.ensureImport(MetaBean.class);
                addLine(1, "public MetaBean metaBean() {");
                addLine(2, "return " + data.getTypeRaw() + ".Meta.INSTANCE;");
            }
            addLine(1, "}");
            addBlankLine();
        } else {
            if (data.isTypeGeneric()) {
                addLine(1, "@SuppressWarnings(\"unchecked\")");
            }
            addLine(1, "@Override");
            addLine(1, "public " + data.getTypeRaw() +
                    ".Meta" + data.getTypeGenericName(true) + " metaBean() {");
            addLine(2, "return " + data.getTypeRaw() + ".Meta.INSTANCE;");
            addLine(1, "}");
            addBlankLine();
        }
    }

    private void generateGettersSetters() {
        for (PropertyGen prop : properties) {
            generateSeparator();
            addLines(prop.generateGetter());
            if (data.isMutable()) {
                addLines(prop.generateSetter());
            }
            if (data.isBeanStyleGenerateProperties()) {
                addLines(prop.generateProperty());
            }
        }
    }

    //-----------------------------------------------------------------------
    private void generateImmutableToBuilder() {
        if (data.isBuilderGenerated()) {
            if (data.isConstructable()) {
                List nonDerived = nonDerivedProperties();
                if (nonDerived.size() > 0 || !data.isTypeFinal()) {
                    addLine(1, "/**");
                    addLine(1, " * Returns a builder that allows this bean to be mutated.");
                    addLine(1, " * @return the mutable builder, not null");
                    addLine(1, " */");
                    if (data.isRootClass() == false) {
                        addLine(1, "@Override");
                    }
                    addLine(1, data.getEffectiveBuilderScope() + data.getEffectiveMinimalBeanBuilderName() + data.getTypeGenericName(true) + " toBuilder() {");
                    addLine(2, "return new " + data.getEffectiveMinimalBeanBuilderName() + data.getTypeGenericDiamond() + "(this);");
                    addLine(1, "}");
                    addBlankLine();
                }
            } else {
                addLine(1, "/**");
                addLine(1, " * Returns a builder that allows this bean to be mutated.");
                addLine(1, " * @return the mutable builder, not null");
                addLine(1, " */");
                if (data.isRootClass() == false) {
                    addLine(1, "@Override");
                }
                addLine(1, "public abstract " + data.getEffectiveMinimalBeanBuilderName() + data.getTypeGenericName(true) + " toBuilder();");
                addBlankLine();
            }
        }
    }

    private void generateClone() {
        if (data.isSkipCloneGeneration() ||
                data.isManualClone() ||
                (data.isRootClass() == false && data.isConstructable() == false)) {
            return;
        }
        addLine(1, "@Override");
        if (data.isImmutable()) {
            addLine(1, "public " + data.getTypeNoExtends() + " clone() {");
            addLine(2, "return this;");
        } else {
            data.ensureImport(JodaBeanUtils.class);
            addLine(1, "public " + data.getTypeNoExtends() + " clone() {");
            addLine(2, "return JodaBeanUtils.cloneAlways(this);");
        }
        addLine(1, "}");
        addBlankLine();
    }

    private void generateEquals() {
        if (data.isManualEqualsHashCode()) {
            return;
        }
        addLine(1, "@Override");
        generateGenerated(1);
        addLine(1, "public boolean equals(Object obj) {");
        addLine(2, "if (obj == this) {");
        addLine(3, "return true;");
        addLine(2, "}");
        addLine(2, "if (obj != null && obj.getClass() == this.getClass()) {");
        List nonDerived = nonDerivedEqualsHashCodeProperties();
        if (nonDerived.size() == 0) {
            if (data.isSubClass()) {
                addLine(3, "return super.equals(obj);");
            } else {
                addLine(3, "return true;");
            }
        } else {
            addLine(3, data.getTypeWildcard() + " other = (" + data.getTypeWildcard() + ") obj;");
            for (int i = 0; i < nonDerived.size(); i++) {
                PropertyGen prop = nonDerived.get(i);
                String getter = equalsHashCodeFieldAccessor(prop);
                data.ensureImport(JodaBeanUtils.class);
                String equals = "JodaBeanUtils.equal(" + getter + ", other." + getter + ")";
                if (PRIMITIVE_EQUALS.contains(prop.getData().getType())) {
                    equals = "(" + getter + " == other." + getter + ")";
                }
                addLine(
                        0, (i == 0 ? "\t\t\treturn " : "\t\t\t\t\t") + equals +
                        (data.isSubClass() || i < nonDerived.size() - 1 ? " &&" : ";"));
            }
            if (data.isSubClass()) {
                addLine(5, "super.equals(obj);");
            }
        }
        addLine(2, "}");
        addLine(2, "return false;");
        addLine(1, "}");
        addBlankLine();
    }

    private void generateHashCode() {
        if (data.isManualEqualsHashCode()) {
            return;
        }
        addLine(1, "@Override");
        generateGenerated(1);
        addLine(1, "public int hashCode() {");
        if (data.isCacheHashCode()) {
            addLine(2, "int hash = " + config.getPrefix() + "cacheHashCode;");
            addLine(2, "if (hash == 0) {");
            if (data.isSubClass()) {
                addLine(3, "hash = 7;");
            } else {
                addLine(3, "hash = getClass().hashCode();");
            }
            generateHashCodeContent("\t\t\t");
            if (data.isSubClass()) {
                addLine(3, "hash = hash ^ super.hashCode();");
            }
            addLine(3, config.getPrefix() + "cacheHashCode = hash;");
            addLine(2, "}");
            addLine(2, "return hash;");
        } else {
            if (data.isSubClass()) {
                addLine(2, "int hash = 7;");
            } else {
                addLine(2, "int hash = getClass().hashCode();");
            }
            generateHashCodeContent("\t\t");
            if (data.isSubClass()) {
                addLine(2, "return hash ^ super.hashCode();");
            } else {
                addLine(2, "return hash;");
            }
        }
        addLine(1, "}");
        addBlankLine();
    }

    private void generateHashCodeContent(String indent) {
        List nonDerived = nonDerivedEqualsHashCodeProperties();
        for (int i = 0; i < nonDerived.size(); i++) {
            PropertyGen prop = nonDerived.get(i);
            String getter = equalsHashCodeFieldAccessor(prop);
            data.ensureImport(JodaBeanUtils.class);
            addLine(0, indent + "hash = hash * 31 + JodaBeanUtils.hashCode(" + getter + ");");
        }
    }

    private String equalsHashCodeFieldAccessor(PropertyGen prop) {
        if (prop.getData().getEqualsHashCodeStyle().equals("field")) {
            return prop.getData().getFieldName();
        } else {
            return prop.getData().getGetterGen().generateGetInvoke(prop.getData());
        }
    }

    private void generateToString() {
        if (data.isManualToStringCode()) {
            return;
        }
        List props = toStringProperties();
        if (data.isRootClass() && data.isTypeFinal()) {
            addLine(1, "@Override");
            generateGenerated(1);
            addLine(1, "public String toString() {");
            addLine(2, "StringBuilder buf = new StringBuilder(" + (props.size() * 32 + 32) + ");");
            addLine(2, "buf.append(\"" + data.getTypeRaw() + "{\");");
            if (props.size() > 0) {
                data.ensureImport(JodaBeanUtils.class);
                for (int i = 0; i < props.size(); i++) {
                    PropertyGen prop = props.get(i);
                    String getter = toStringFieldAccessor(prop);
                    addLine(2, "buf.append(\"" + prop.getData().getPropertyName() + "\").append('=')" + 
                            join(i, props,
                                    ".append(JodaBeanUtils.toString(" + getter + ")).append(',').append(' ');",
                                    ".append(JodaBeanUtils.toString(" + getter + "));"));
                }
            }
            addLine(2, "buf.append('}');");
            addLine(2, "return buf.toString();");
            addLine(1, "}");
            addBlankLine();
            return;
        }
        
        addLine(1, "@Override");
        generateGenerated(1);
        addLine(1, "public String toString() {");
        addLine(2, "StringBuilder buf = new StringBuilder(" + (props.size() * 32 + 32) + ");");
        addLine(2, "buf.append(\"" + data.getTypeRaw() + "{\");");
        addLine(2, "int len = buf.length();");
        addLine(2, "toString(buf);");
        addLine(2, "if (buf.length() > len) {");
        addLine(3, "buf.setLength(buf.length() - 2);");
        addLine(2, "}");
        addLine(2, "buf.append('}');");
        addLine(2, "return buf.toString();");
        addLine(1, "}");
        addBlankLine();
        
        if (data.isSubClass()) {
            addLine(1, "@Override");
        }
        generateGenerated(1);
        addLine(1, "protected void toString(StringBuilder buf) {");
        if (data.isSubClass()) {
            addLine(2, "super.toString(buf);");
        }
        for (int i = 0; i < props.size(); i++) {
            PropertyGen prop = props.get(i);
            String getter = toStringFieldAccessor(prop);
            data.ensureImport(JodaBeanUtils.class);
            addLine(2, "buf.append(\"" + prop.getData().getPropertyName() +
                    "\").append('=').append(JodaBeanUtils.toString(" + getter + ")).append(',').append(' ');");
        }
        addLine(1, "}");
        addBlankLine();
    }

    private String toStringFieldAccessor(PropertyGen prop) {
        if (prop.getData().isDerived()) {
            return prop.getData().getGetterGen().generateGetInvoke(prop.getData());
        } else if (prop.getData().getToStringStyle().equals("field")) {
            return prop.getData().getFieldName();
        } else {
            return prop.getData().getGetterGen().generateGetInvoke(prop.getData());
        }
    }

    //-----------------------------------------------------------------------
    private void generateMetaClass() {
        if (data.isBeanStyleLightOrMinimal()) {
            return;
        }
        generateSeparator();
        addLine(1, "/**");
        addLine(1, " * The meta-bean for {@code " + data.getTypeRaw() + "}.");
        if (data.isTypeGeneric()) {
            for (int j = 0; j < data.getTypeGenericCount(); j++) {
                addLine(1, " * @param " + data.getTypeGenericName(j, true) + "  the type");
            }
        }
        addLine(1, " */");
        generateGenerated(1);
        String superMeta;
        if (data.isSubClass()) {
            superMeta = data.getSuperTypeRaw() + ".Meta" + data.getSuperTypeGeneric(true);
        } else {
            data.ensureImport(DirectMetaBean.class);
            superMeta = "DirectMetaBean";
        }
        String finalType = data.isTypeFinal() ? "final " : "";
        String implementClause = data.getBeanMetaImplements().isEmpty() ? "" : " implements " + data.getBeanMetaImplements();
        if (data.isTypeGeneric()) {
            addLine(1, data.getEffectiveMetaScope() + "static " + finalType + 
                    "class Meta" + data.getTypeGeneric(true) + " extends " + superMeta + implementClause + " {");
        } else {
            addLine(1, data.getEffectiveMetaScope() + "static " + finalType + 
                    "class Meta extends " + superMeta + implementClause + " {");
        }
        addLine(2, "/**");
        addLine(2, " * The singleton instance of the meta-bean.");
        addLine(2, " */");
        if (data.isTypeGeneric()) {
            addLine(2, "@SuppressWarnings(\"rawtypes\")");
        }
        addLine(2, "static final Meta INSTANCE = new Meta();");
        addBlankLine();
        generateMetaPropertyConstants();
        generateMetaPropertyMapSetup();
        addLine(2, "/**");
        addLine(2, " * Restricted constructor.");
        addLine(2, " */");
        addLine(2, data.getNestedClassConstructorScope() + " Meta() {");
        addLine(2, "}");
        addBlankLine();
        generateMetaPropertyGet();
        generateMetaBuilder();
        generateMetaBeanType();
        generateMetaPropertyMap();
        generateIndentedSeparator();
        generateMetaPropertyMethods();
        generateIndentedSeparator();
        generateMetaGetPropertyValue();
        generateMetaSetPropertyValue();
        generateMetaValidate();
        addLine(1, "}");
        addBlankLine();
    }

    private void generateMetaPropertyConstants() {
        for (PropertyGen prop : properties) {
            addLines(prop.generateMetaPropertyConstant());
        }
    }

    private void generateMetaPropertyMapSetup() {
        data.ensureImport(MetaProperty.class);
        data.ensureImport(DirectMetaPropertyMap.class);
        addLine(2, "/**");
        addLine(2, " * The meta-properties.");
        addLine(2, " */");
        addLine(2, "private final Map> " + config.getPrefix() + "metaPropertyMap$ = new DirectMetaPropertyMap(");
        if (data.isSubClass()) {
            addLine(4, "this, (DirectMetaPropertyMap) super.metaPropertyMap()" + (properties.size() == 0 ? ");" : ","));
        } else {
            addLine(4, "this, null" + (properties.size() == 0 ? ");" : ","));
        }
        for (int i = 0; i < properties.size(); i++) {
            addLine(4, "\"" + properties.get(i).getData().getPropertyName() + "\"" + joinComma(i, properties, ");"));
        }
        addBlankLine();
    }

    private void generateMetaBuilder() {
        if (!data.isConstructable()) {
            addLine(2, "@Override");
            addLine(2, "public boolean isBuildable() {");
            addLine(3, "return false;");
            addLine(2, "}");
            addBlankLine();
        }
        addLine(2, "@Override");
        if (data.isImmutable() && data.isEffectiveBuilderScopeVisible() == false) {
            data.ensureImport(BeanBuilder.class);
            addLine(2, "public BeanBuilder builder() {");
            if (data.isConstructable()) {
                addLine(3, "return new " + data.getEffectiveBeanBuilderName() + data.getTypeGenericDiamond() + "();");
            } else {
                addLine(3, "throw new UnsupportedOperationException(\"" + data.getTypeRaw() + " is an abstract class\");");
            }
        } else if (data.isImmutable() || (data.isMutable() && data.isBuilderScopeVisible())) {
            addLine(2, "public " + data.getEffectiveBeanBuilderName() + data.getTypeGenericName(true) + " builder() {");
            if (data.isConstructable()) {
                addLine(3, "return new " + data.getEffectiveBeanBuilderName() + data.getTypeGenericDiamond() + "();");
            } else {
                addLine(3, "throw new UnsupportedOperationException(\"" + data.getTypeRaw() + " is an abstract class\");");
            }
        } else {
            data.ensureImport(BeanBuilder.class);
            addLine(2, "public BeanBuilder builder() {");
            if (data.isConstructable()) {
                data.ensureImport(DirectBeanBuilder.class);
                addLine(3, "return new DirectBeanBuilder<>(new " + data.getTypeNoExtends() + "());");
            } else {
                addLine(3, "throw new UnsupportedOperationException(\"" + data.getTypeRaw() + " is an abstract class\");");
            }
        }
        addLine(2, "}");
        addBlankLine();
    }

    private void generateMetaBeanType() {
        if (data.isTypeGeneric()) {
            addLine(2, "@SuppressWarnings({\"unchecked\", \"rawtypes\" })");
        }
        addLine(2, "@Override");
        addLine(2, "public Class beanType() {");
        if (data.isTypeGeneric()) {
            addLine(3, "return (Class) " + data.getTypeRaw() + ".class;");
        } else {
            addLine(3, "return " + data.getTypeNoExtends() + ".class;");
        }
        addLine(2, "}");
        addBlankLine();
    }

    private void generateMetaPropertyGet() {
        if (properties.size() > 0) {
            data.ensureImport(MetaProperty.class);
            addLine(2, "@Override");
            addLine(2, "protected MetaProperty metaPropertyGet(String propertyName) {");
            addLine(3, "switch (propertyName.hashCode()) {");
            for (PropertyGen prop : properties) {
                addLines(prop.generateMetaPropertyGetCase());
            }
            addLine(3, "}");
            addLine(3, "return super.metaPropertyGet(propertyName);");
            addLine(2, "}");
            addBlankLine();
        }
    }

    private void generateMetaPropertyMap() {
        data.ensureImport(Map.class);
        addLine(2, "@Override");
        addLine(2, "public Map> metaPropertyMap() {");
        addLine(3, "return " + config.getPrefix() + "metaPropertyMap$;");
        addLine(2, "}");
        addBlankLine();
    }

    private void generateMetaPropertyMethods() {
        if (data.isBeanStyleGenerateMetaProperties()) {
            for (PropertyGen prop : properties) {
                addLines(prop.generateMetaProperty());
            }
        }
    }

    //-----------------------------------------------------------------------
    private void generateMetaGetPropertyValue() {
        if (properties.size() == 0) {
            return;
        }
        data.ensureImport(Bean.class);
        addLine(2, "@Override");
        addLine(2, "protected Object propertyGet(Bean bean, String propertyName, boolean quiet) {");
        addLine(3, "switch (propertyName.hashCode()) {");
        for (PropertyGen prop : properties) {
            addLines(prop.generatePropertyGetCase());
        }
        addLine(3, "}");
        addLine(3, "return super.propertyGet(bean, propertyName, quiet);");
        addLine(2, "}");
        addBlankLine();
    }

    private void generateMetaSetPropertyValue() {
        if (properties.size() == 0) {
            return;
        }
        data.ensureImport(Bean.class);
        if (data.isImmutable()) {
            addLine(2, "@Override");
            addLine(2, "protected void propertySet(Bean bean, String propertyName, Object newValue, boolean quiet) {");
            addLine(3, "metaProperty(propertyName);");
            addLine(3, "if (quiet) {");
            addLine(4, "return;");
            addLine(3, "}");
            addLine(3, "throw new UnsupportedOperationException(\"Property cannot be written: \" + propertyName);");
            addLine(2, "}");
            addBlankLine();
            return;
        }
        
        boolean generics = false;
        for (PropertyData prop : data.getProperties()) {
            generics |= (prop.getStyle().isWritable() &&
                    ((prop.isGeneric() && prop.isGenericWildcardParamType() == false) || data.isTypeGeneric()));
        }
        if (generics) {
            addLine(2, "@SuppressWarnings(\"unchecked\")");
        }
        addLine(2, "@Override");
        addLine(2, "protected void propertySet(Bean bean, String propertyName, Object newValue, boolean quiet) {");
        addLine(3, "switch (propertyName.hashCode()) {");
        for (PropertyGen prop : properties) {
            addLines(prop.generatePropertySetCase());
        }
        addLine(3, "}");
        addLine(3, "super.propertySet(bean, propertyName, newValue, quiet);");
        addLine(2, "}");
        addBlankLine();
    }

    private void generateMetaValidate() {
        if (data.isValidated() == false || data.isImmutable()) {
            return;
        }
        data.ensureImport(Bean.class);
        addLine(2, "@Override");
        addLine(2, "protected void validate(Bean bean) {");
        if (data.isValidated()) {
            for (PropertyGen prop : properties) {
                if (prop.getData().isValidated()) {
                    addLine(3, prop.getData().getValidationMethodName() +
                            "(((" + data.getTypeWildcard() + ") bean)." + prop.getData().getFieldName() +
                            ", \"" + prop.getData().getPropertyName() + "\");");
                }
            }
        }
        if (data.isSubClass()) {
            addLine(3, "super.validate(bean);");
        }
        addLine(2, "}");
        addBlankLine();
    }

    //-----------------------------------------------------------------------
    private void generateBuilderClass() {
        if (data.isSkipBuilderGeneration()) {
            return;
        }
        List nonDerived = nonDerivedProperties();
        generateSeparator();
        String finalType = data.isTypeFinal() ? "final " : "";
        addLine(1, "/**");
        addLine(1, " * The bean-builder for {@code " + data.getTypeRaw() + "}.");
        if (data.isTypeGeneric()) {
            for (int j = 0; j < data.getTypeGenericCount(); j++) {
                addLine(1, " * @param " + data.getTypeGenericName(j, true) + "  the type");
            }
        }
        addLine(1, " */");
        generateGenerated(1);
        String superBuilder;
        if (data.isSubClass()) {
            superBuilder = data.getSuperTypeRaw() + ".Builder" + data.getSuperTypeGeneric(true);
        } else if (data.isEffectiveBuilderScopeVisible()) {
            data.ensureImport(DirectFieldsBeanBuilder.class);
            superBuilder = "DirectFieldsBeanBuilder<" + data.getTypeNoExtends() + ">";
        } else {
            data.ensureImport(DirectPrivateBeanBuilder.class);
            superBuilder = "DirectPrivateBeanBuilder<" + data.getTypeNoExtends() + ">";
        }
        if (data.isConstructable()) {
            addLine(1, data.getEffectiveBuilderScope() + "static " + finalType +
                    "class Builder" + data.getTypeGeneric(true) + " extends " + superBuilder + " {");
        } else {
            addLine(1, data.getEffectiveBuilderScope() + "abstract static " + finalType +
                    "class Builder" + data.getTypeGeneric(true) + " extends " + superBuilder + " {");
        }
        if (nonDerived.size() > 0) {
            addBlankLine();
            generateBuilderProperties();
        }
        addBlankLine();
        generateBuilderConstructorNoArgs();
        generateBuilderConstructorCopy();
        generateIndentedSeparator();
        generateBuilderGet();
        generateBuilderSet();
        generateBuilderOtherSets();
        if (data.isConstructable()) {
            generateBuilderBuild();
        }
        generateIndentedSeparator();
        generateBuilderPropertySetMethods();
        generateIndentedSeparator();
        generateBuilderToString();
        addLine(1, "}");
        addBlankLine();
    }

    private void generateBuilderConstructorNoArgs() {
        addLine(2, "/**");
        addLine(2, " * Restricted constructor.");
        addLine(2, " */");
        addLine(2, data.getNestedClassConstructorScope() + " Builder() {");
        if (data.getImmutableDefaults() != null) {
            addLine(3, data.getImmutableDefaults() + "(this);");
        }
        addLine(2, "}");
        addBlankLine();
    }

    private void generateBuilderConstructorCopy() {
        if (data.isBuilderGenerated()) {
            List nonDerived = nonDerivedProperties();
            if (nonDerived.size() > 0 || !data.isTypeFinal()) {
                addLine(2, "/**");
                addLine(2, " * Restricted copy constructor.");
                addLine(2, " * @param beanToCopy  the bean to copy from, not null");
                addLine(2, " */");
                addLine(2, data.getNestedClassConstructorScope() + " Builder(" + data.getTypeNoExtends() + " beanToCopy) {");
                if (data.isSubClass()) {
                    addLine(3, "super(beanToCopy);");
                }
                for (int i = 0; i < nonDerived.size(); i++) {
                    addLines(nonDerived.get(i).generateBuilderConstructorAssign("beanToCopy"));
                }
                addLine(2, "}");
                addBlankLine();
            }
        }
    }

    private void generateBuilderProperties() {
        for (PropertyGen prop : nonDerivedProperties()) {
            addLines(prop.generateBuilderField());
        }
    }

    private void generateBuilderGet() {
        List nonDerived = nonDerivedProperties();
        addLine(2, "@Override");
        addLine(2, "public Object get(String propertyName) {");
        if (nonDerived.size() > 0) {
            addLine(3, "switch (propertyName.hashCode()) {");
            for (PropertyGen prop : nonDerived) {
                addLines(prop.generateBuilderFieldGet());
            }
            addLine(4, "default:");
            if (data.isRootClass()) {
                data.ensureImport(NoSuchElementException.class);
                addLine(5, "throw new NoSuchElementException(\"Unknown property: \" + propertyName);");
            } else {
                addLine(5, "return super.get(propertyName);");
            }
            addLine(3, "}");
        } else {
            if (!data.isRootClass()) {
                addLine(3, "return super.get(propertyName);");
            } else {
                data.ensureImport(NoSuchElementException.class);
                addLine(3, "throw new NoSuchElementException(\"Unknown property: \" + propertyName);");
            }
        }
        addLine(2, "}");
        addBlankLine();
    }

    private void generateBuilderSet() {
        List nonDerived = nonDerivedProperties();
        boolean generics = data.getProperties().stream()
                .filter(p -> p.isGeneric() && p.isGenericWildcardParamType() == false)
                .findAny()
                .isPresent();
        if (generics) {
            addLine(2, "@SuppressWarnings(\"unchecked\")");
        }
        addLine(2, "@Override");
        addLine(2, "public Builder" + data.getTypeGenericName(true) + " set(String propertyName, Object newValue) {");
        if (nonDerived.size() > 0) {
            addLine(3, "switch (propertyName.hashCode()) {");
            for (PropertyGen prop : nonDerived) {
                addLines(prop.generateBuilderFieldSet());
            }
            addLine(4, "default:");
            if (data.isRootClass()) {
                data.ensureImport(NoSuchElementException.class);
                addLine(5, "throw new NoSuchElementException(\"Unknown property: \" + propertyName);");
            } else {
                addLine(5, "super.set(propertyName, newValue);");
                addLine(5, "break;");
            }
            addLine(3, "}");
            addLine(3, "return this;");
        } else {
            if (!data.isRootClass()) {
                addLine(3, "super.set(propertyName, newValue);");
                addLine(3, "return this;");
            } else {
                data.ensureImport(NoSuchElementException.class);
                addLine(3, "throw new NoSuchElementException(\"Unknown property: \" + propertyName);");
            }
        }
        addLine(2, "}");
        addBlankLine();
    }

    private void generateBuilderOtherSets() {
        if (data.isEffectiveBuilderScopeVisible()) {
            addLine(2, "@Override");
            addLine(2, "public Builder" + data.getTypeGenericName(true) + " set(MetaProperty property, Object value) {");
            addLine(3, "super.set(property, value);");
            addLine(3, "return this;");
            addLine(2, "}");
            addBlankLine();
        }
    }

    private void generateBuilderBuild() {
        List nonDerived = nonDerivedProperties();
        addLine(2, "@Override");
        addLine(2, "public " + data.getTypeRaw() + data.getTypeGenericName(true) + " build() {");
        if (data.getImmutablePreBuild() != null) {
            addLine(3, data.getImmutablePreBuild() + "(this);");
        }
        if (data.getConstructorStyle() == CONSTRUCTOR_BY_ARGS) {
            if (nonDerived.size() == 0) {
                addLine(3, "return new " + data.getTypeWithDiamond() + "();");
            } else {
                addLine(3, "return new " + data.getTypeWithDiamond() + "(");
                for (int i = 0; i < nonDerived.size(); i++) {
                    addLine(5, nonDerived.get(i).generateBuilderFieldName() + joinComma(i, nonDerived, ");"));
                }
            }
        } else if (data.getConstructorStyle() == CONSTRUCTOR_BY_BUILDER) {
            addLine(3, "return new " + data.getTypeWithDiamond() + "(this);");
        }
        addLine(2, "}");
        addBlankLine();
    }

    private void generateBuilderPropertySetMethods() {
        if (data.isEffectiveBuilderScopeVisible()) {
            for (PropertyGen prop : nonDerivedProperties()) {
                addLines(prop.generateBuilderSetMethod());
            }
        }
    }

    private void generateBuilderToString() {
        List nonDerived = toStringProperties();
        if (data.isImmutable() && data.isTypeFinal()) {
            addLine(2, "@Override");
            addLine(2, "public String toString() {");
            if (nonDerived.size() == 0) {
                addLine(3, "return \"" + data.getEffectiveBeanBuilderName() + "{}\";");
            } else {
                addLine(3, "StringBuilder buf = new StringBuilder(" + (nonDerived.size() * 32 + 32) + ");");
                addLine(3, "buf.append(\"" + data.getEffectiveBeanBuilderName() + "{\");");
                for (int i = 0; i < nonDerived.size(); i++) {
                    PropertyGen prop = nonDerived.get(i);
                    String getter = nonDerived.get(i).generateBuilderFieldName();
                    data.ensureImport(JodaBeanUtils.class);
                    String base = "\t\t\tbuf.append(\"" + prop.getData().getPropertyName() +
                            "\").append('=').append(JodaBeanUtils.toString(" + getter + "))";
                    addLine(0, base + join(i, nonDerived, ".append(',').append(' ');", ";"));
                }
                addLine(3, "buf.append('}');");
                addLine(3, "return buf.toString();");
            }
            addLine(2, "}");
            addBlankLine();
            return;
        }
        
        addLine(2, "@Override");
        addLine(2, "public String toString() {");
        addLine(3, "StringBuilder buf = new StringBuilder(" + (nonDerived.size() * 32 + 32) + ");");
        addLine(3, "buf.append(\"" + data.getEffectiveBeanBuilderName() + "{\");");
        addLine(3, "int len = buf.length();");
        addLine(3, "toString(buf);");
        addLine(3, "if (buf.length() > len) {");
        addLine(4, "buf.setLength(buf.length() - 2);");
        addLine(3, "}");
        addLine(3, "buf.append('}');");
        addLine(3, "return buf.toString();");
        addLine(2, "}");
        addBlankLine();
        
        if (data.isSubClass()) {
            addLine(2, "@Override");
        }
        addLine(2, "protected void toString(StringBuilder buf) {");
        if (data.isSubClass()) {
            addLine(3, "super.toString(buf);");
        }
        for (int i = 0; i < nonDerived.size(); i++) {
            PropertyGen prop = nonDerived.get(i);
            String getter = nonDerived.get(i).generateBuilderFieldName();
            data.ensureImport(JodaBeanUtils.class);
            addLine(3, "buf.append(\"" + prop.getData().getPropertyName() +
                    "\").append('=').append(JodaBeanUtils.toString(" + getter + ")).append(',').append(' ');");
        }
        addLine(2, "}");
        addBlankLine();
    }

    //-----------------------------------------------------------------------
    private void addLines(List lines) {
        insertRegion.addAll(lines);
    }

    private void addLine(int tabCount, String line) {
        StringBuilder buf = new StringBuilder(line.length() + tabCount);
        for (int i = 0; i < tabCount; i++) {
            buf.append('\t');
        }
        buf.append(line);
        insertRegion.add(buf.toString());
    }

    private void addBlankLine() {
        insertRegion.add("");
    }

    private static String join(int i, List list, String join, String end) {
        return (i < list.size() - 1 ? join : end);
    }

    private static String joinComma(int i, List list, String end) {
        return join(i, list, ",", end);
    }

    boolean isBean() {
        return properties != null;
    }

    BeanData getData() {
        return data;
    }

    BeanGenConfig getConfig() {
        return config;
    }

    File getFile() {
        return file;
    }

    String getFieldPrefix() {
        return config.getPrefix();
    }

    private List nonDerivedProperties() {
        List nonDerived = new ArrayList<>();
        for (PropertyGen prop : properties) {
            if (prop.getData().isDerived() == false) {
                nonDerived.add(prop);
            }
        }
        return nonDerived;
    }

    private List nonDerivedEqualsHashCodeProperties() {
        List nonDerived = new ArrayList<>();
        for (PropertyGen prop : properties) {
            if (!prop.getData().isDerived() && !prop.getData().getEqualsHashCodeStyle().equals("omit")) {
                nonDerived.add(prop);
            }
        }
        return nonDerived;
    }

    private List toStringProperties() {
        List props = new ArrayList<>();
        for (PropertyGen prop : properties) {
            if (!"omit".equals(prop.getData().getToStringStyle())) {
                props.add(prop);
            }
        }
        return props;
    }

}