package org.codehaus.modello.plugin.java;

/*
 * Copyright (c) 2004, Codehaus.org
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is furnished to do
 * so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

import javax.inject.Named;

import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.codehaus.modello.ModelloException;
import org.codehaus.modello.ModelloRuntimeException;
import org.codehaus.modello.model.CodeSegment;
import org.codehaus.modello.model.Model;
import org.codehaus.modello.model.ModelAssociation;
import org.codehaus.modello.model.ModelClass;
import org.codehaus.modello.model.ModelDefault;
import org.codehaus.modello.model.ModelField;
import org.codehaus.modello.model.ModelInterface;
import org.codehaus.modello.plugin.java.javasource.JArrayType;
import org.codehaus.modello.plugin.java.javasource.JClass;
import org.codehaus.modello.plugin.java.javasource.JCollectionType;
import org.codehaus.modello.plugin.java.javasource.JConstructor;
import org.codehaus.modello.plugin.java.javasource.JDocDescriptor;
import org.codehaus.modello.plugin.java.javasource.JField;
import org.codehaus.modello.plugin.java.javasource.JInterface;
import org.codehaus.modello.plugin.java.javasource.JMapType;
import org.codehaus.modello.plugin.java.javasource.JMethod;
import org.codehaus.modello.plugin.java.javasource.JMethodSignature;
import org.codehaus.modello.plugin.java.javasource.JParameter;
import org.codehaus.modello.plugin.java.javasource.JSourceCode;
import org.codehaus.modello.plugin.java.javasource.JSourceWriter;
import org.codehaus.modello.plugin.java.javasource.JType;
import org.codehaus.modello.plugin.java.metadata.JavaAssociationMetadata;
import org.codehaus.modello.plugin.java.metadata.JavaClassMetadata;
import org.codehaus.modello.plugin.java.metadata.JavaFieldMetadata;
import org.codehaus.modello.plugin.model.ModelClassMetadata;
import org.codehaus.plexus.util.StringUtils;

/**
 * @author <a href="mailto:jason@modello.org">Jason van Zyl</a>
 */
@Named("java")
public class JavaModelloGenerator extends AbstractJavaModelloGenerator {

    private Collection<String> immutableTypes = new HashSet<String>(Arrays.asList(new String[] {
        "boolean",
        "Boolean",
        "byte",
        "Byte",
        "char",
        "Character",
        "short",
        "Short",
        "int",
        "Integer",
        "long",
        "Long",
        "float",
        "Float",
        "double",
        "Double",
        "String"
    }));

    public void generate(Model model, Map<String, Object> parameters) throws ModelloException {
        initialize(model, parameters);

        try {
            generateJava();
        } catch (IOException ex) {
            throw new ModelloException("Exception while generating Java.", ex);
        }
    }

    private void generateJava() throws ModelloException, IOException {
        Model objectModel = getModel();

        ModelClass locationTrackerClass = objectModel.getLocationTracker(getGeneratedVersion());
        ModelClass sourceTrackerClass = objectModel.getSourceTracker(getGeneratedVersion());

        // ----------------------------------------------------------------------
        // Generate the interfaces.
        // ----------------------------------------------------------------------

        for (ModelInterface modelInterface : objectModel.getInterfaces(getGeneratedVersion())) {
            generateInterface(modelInterface);
        }

        String locationTrackerInterface = generateLocationTracker(objectModel, locationTrackerClass);

        // ----------------------------------------------------------------------
        // Generate the classes.
        // ----------------------------------------------------------------------

        for (ModelClass modelClass : objectModel.getClasses(getGeneratedVersion())) {
            JavaClassMetadata javaClassMetadata = (JavaClassMetadata) modelClass.getMetadata(JavaClassMetadata.ID);

            if (!javaClassMetadata.isEnabled()) {
                // Skip generation of those classes that are not enabled for the java plugin.
                continue;
            }

            String packageName = modelClass.getPackageName(isPackageWithVersion(), getGeneratedVersion());

            JSourceWriter sourceWriter = newJSourceWriter(packageName, modelClass.getName());

            JClass jClass = new JClass(packageName + '.' + modelClass.getName());

            initHeader(jClass);

            suppressAllWarnings(objectModel, jClass);

            if (StringUtils.isNotEmpty(modelClass.getDescription())) {
                jClass.getJDocComment().setComment(appendPeriod(modelClass.getDescription()));
            }

            addModelImports(jClass, modelClass);

            jClass.getModifiers().setAbstract(javaClassMetadata.isAbstract());

            boolean superClassInModel = false;
            if (modelClass.getSuperClass() != null) {
                jClass.setSuperClass(modelClass.getSuperClass());
                superClassInModel = isClassInModel(modelClass.getSuperClass(), objectModel);
            }

            for (String implementedInterface : modelClass.getInterfaces()) {
                jClass.addInterface(implementedInterface);
            }

            jClass.addInterface(Serializable.class.getName());

            if (!modelClass.getAnnotations().isEmpty()) {
                for (String annotation : modelClass.getAnnotations()) {
                    jClass.appendAnnotation(annotation);
                }
            }

            JSourceCode jConstructorSource = new JSourceCode();

            for (ModelField modelField : modelClass.getFields(getGeneratedVersion())) {
                if (modelField instanceof ModelAssociation) {
                    createAssociation(jClass, (ModelAssociation) modelField, jConstructorSource);
                } else {
                    createField(jClass, modelField);
                }
            }

            // since 1.8
            // needed to understand if the instance can be created with empty ctor or not
            JConstructor jConstructor = null;

            if (!jConstructorSource.isEmpty()) {
                // Ironic that we are doing lazy init huh?
                jConstructor = jClass.createConstructor();
                jConstructor.setSourceCode(jConstructorSource);
                jClass.addConstructor(jConstructor);
            }

            // ----------------------------------------------------------------------
            // equals() / hashCode() / toString()
            // ----------------------------------------------------------------------

            List<ModelField> identifierFields = modelClass.getIdentifierFields(getGeneratedVersion());

            if (identifierFields.size() != 0) {
                JMethod equals = generateEquals(modelClass);

                jClass.addMethod(equals);

                JMethod hashCode = generateHashCode(modelClass);

                jClass.addMethod(hashCode);

                // backward compat
                if (!javaClassMetadata.isGenerateToString()) {
                    JMethod toString = generateToString(modelClass, true);

                    jClass.addMethod(toString);
                }
            }

            if (javaClassMetadata.isGenerateToString()) {

                JMethod toString = generateToString(modelClass, false);

                jClass.addMethod(toString);
            }

            // ----------------------------------------------------------------------
            // Model.Builder
            // since 1.8
            // ----------------------------------------------------------------------

            if (javaClassMetadata.isGenerateBuilder()) {
                generateBuilder(modelClass, jClass.createInnerClass("Builder"), jConstructor);
            }

            // ----------------------------------------------------------------------
            // Model.newXXXInstance
            // since 1.8
            // ----------------------------------------------------------------------

            if (javaClassMetadata.isGenerateStaticCreators()) {
                generateStaticCreator(modelClass, jClass, jConstructor);
            }

            boolean cloneLocations = !superClassInModel && modelClass != sourceTrackerClass;
            JMethod[] cloneMethods = generateClone(modelClass, cloneLocations ? locationTrackerClass : null);
            if (cloneMethods.length > 0) {
                jClass.addInterface(Cloneable.class.getName());
                jClass.addMethods(cloneMethods);
            }

            if (modelClass.getCodeSegments(getGeneratedVersion()) != null) {
                for (CodeSegment codeSegment : modelClass.getCodeSegments(getGeneratedVersion())) {
                    jClass.addSourceCode(codeSegment.getCode());
                }
            }

            ModelClassMetadata modelClassMetadata = (ModelClassMetadata) modelClass.getMetadata(ModelClassMetadata.ID);

            if (modelClassMetadata != null) {
                if (modelClassMetadata.isRootElement()) {
                    ModelField modelEncoding = new ModelField(modelClass, "modelEncoding");
                    modelEncoding.setType("String");
                    modelEncoding.setDefaultValue("UTF-8");
                    modelEncoding.addMetadata(new JavaFieldMetadata());
                    createField(jClass, modelEncoding);
                }
            }

            if (modelClass == locationTrackerClass) {
                jClass.addInterface(locationTrackerInterface);

                generateLocationBean(jClass, modelClass, sourceTrackerClass);

                generateLocationTracking(jClass, modelClass, locationTrackerClass);
            } else if (locationTrackerClass != null && modelClass != sourceTrackerClass && !superClassInModel) {
                jClass.addInterface(locationTrackerInterface);

                generateLocationTracking(jClass, modelClass, locationTrackerClass);
            }

            jClass.print(sourceWriter);

            sourceWriter.close();
        }
    }

    private void generateInterface(ModelInterface modelInterface) throws ModelloException, IOException {
        Model objectModel = modelInterface.getModel();

        String packageName = modelInterface.getPackageName(isPackageWithVersion(), getGeneratedVersion());

        JSourceWriter sourceWriter = newJSourceWriter(packageName, modelInterface.getName());

        JInterface jInterface = new JInterface(packageName + '.' + modelInterface.getName());

        initHeader(jInterface);

        suppressAllWarnings(objectModel, jInterface);

        if (modelInterface.getSuperInterface() != null) {
            // check if we need an import: if it is a generated superInterface in another package
            try {
                ModelInterface superInterface =
                        objectModel.getInterface(modelInterface.getSuperInterface(), getGeneratedVersion());
                String superPackageName = superInterface.getPackageName(isPackageWithVersion(), getGeneratedVersion());

                if (!packageName.equals(superPackageName)) {
                    jInterface.addImport(superPackageName + '.' + superInterface.getName());
                }
            } catch (ModelloRuntimeException mre) {
                // no problem if the interface does not exist in the model, it can be in the jdk
            }

            jInterface.addInterface(modelInterface.getSuperInterface());
        }

        if (modelInterface.getCodeSegments(getGeneratedVersion()) != null) {
            for (CodeSegment codeSegment : modelInterface.getCodeSegments(getGeneratedVersion())) {
                jInterface.addSourceCode(codeSegment.getCode());
            }
        }

        if (!modelInterface.getAnnotations().isEmpty()) {
            for (String annotation : modelInterface.getAnnotations()) {
                jInterface.appendAnnotation(annotation);
            }
        }

        jInterface.print(sourceWriter);

        sourceWriter.close();
    }

    private JMethod generateEquals(ModelClass modelClass) {
        JMethod equals = new JMethod("equals", JType.BOOLEAN, null);

        equals.addParameter(new JParameter(new JClass("Object"), "other"));

        JSourceCode sc = equals.getSourceCode();

        sc.add("if ( this == other )");
        sc.add("{");
        sc.addIndented("return true;");
        sc.add("}");
        sc.add("");
        sc.add("if ( !( other instanceof " + modelClass.getName() + " ) )");
        sc.add("{");
        sc.addIndented("return false;");
        sc.add("}");
        sc.add("");
        sc.add(modelClass.getName() + " that = (" + modelClass.getName() + ") other;");
        sc.add("boolean result = true;");

        sc.add("");

        for (ModelField identifier : modelClass.getIdentifierFields(getGeneratedVersion())) {
            String name = identifier.getName();
            if ("boolean".equals(identifier.getType())
                    || "byte".equals(identifier.getType())
                    || "char".equals(identifier.getType())
                    || "double".equals(identifier.getType())
                    || "float".equals(identifier.getType())
                    || "int".equals(identifier.getType())
                    || "short".equals(identifier.getType())
                    || "long".equals(identifier.getType())) {
                sc.add("result = result && " + name + " == that." + name + ";");
            } else {
                name = "get" + capitalise(name) + "()";
                sc.add("result = result && ( " + name + " == null ? that." + name + " == null : " + name
                        + ".equals( that." + name + " ) );");
            }
        }

        if (modelClass.getSuperClass() != null) {
            sc.add("result = result && ( super.equals( other ) );");
        }

        sc.add("");

        sc.add("return result;");

        return equals;
    }

    private JMethod generateToString(ModelClass modelClass, boolean onlyIdentifierFields) {
        JMethod toString = new JMethod("toString", new JType(String.class.getName()), null);

        List<ModelField> fields = onlyIdentifierFields
                ? modelClass.getIdentifierFields(getGeneratedVersion())
                : modelClass.getFields(getGeneratedVersion());

        JSourceCode sc = toString.getSourceCode();

        if (fields.size() == 0) {
            sc.add("return super.toString();");

            return toString;
        }

        sc.add("StringBuilder buf = new StringBuilder( 128 );");

        sc.add("");

        for (Iterator<ModelField> j = fields.iterator(); j.hasNext(); ) {
            ModelField identifier = j.next();

            String getter = "boolean".equals(identifier.getType()) ? "is" : "get";

            sc.add("buf.append( \"" + identifier.getName() + " = '\" );");
            sc.add("buf.append( " + getter + capitalise(identifier.getName()) + "() );");
            sc.add("buf.append( \"'\" );");

            if (j.hasNext()) {
                sc.add("buf.append( \"\\n\" ); ");
            }
        }

        if (modelClass.getSuperClass() != null) {
            sc.add("buf.append( \"\\n\" );");
            sc.add("buf.append( super.toString() );");
        }

        sc.add("");

        sc.add("return buf.toString();");

        return toString;
    }

    private JMethod generateHashCode(ModelClass modelClass) {
        JMethod hashCode = new JMethod("hashCode", JType.INT, null);

        List<ModelField> identifierFields = modelClass.getIdentifierFields(getGeneratedVersion());

        JSourceCode sc = hashCode.getSourceCode();

        if (identifierFields.size() == 0) {
            sc.add("return super.hashCode();");

            return hashCode;
        }

        sc.add("int result = 17;");

        sc.add("");

        for (ModelField identifier : identifierFields) {
            sc.add("result = 37 * result + " + createHashCodeForField(identifier) + ";");
        }

        if (modelClass.getSuperClass() != null) {
            sc.add("result = 37 * result + super.hashCode();");
        }

        sc.add("");

        sc.add("return result;");

        return hashCode;
    }

    private JMethod[] generateClone(ModelClass modelClass, ModelClass locationClass) throws ModelloException {
        String cloneModeClass = getCloneMode(modelClass);

        if (JavaClassMetadata.CLONE_NONE.equals(cloneModeClass)) {
            return new JMethod[0];
        }

        JType returnType = new JClass(modelClass.getName());

        JMethod cloneMethod = new JMethod("clone", returnType, null);

        JSourceCode sc = cloneMethod.getSourceCode();

        sc.add("try");
        sc.add("{");
        sc.indent();

        sc.add(modelClass.getName() + " copy = (" + modelClass.getName() + ") super.clone();");

        sc.add("");

        for (ModelField modelField : modelClass.getFields(getGeneratedVersion())) {
            String thisField = "this." + modelField.getName();
            String copyField = "copy." + modelField.getName();

            if ("DOM".equals(modelField.getType())) {
                sc.add("if ( " + thisField + " != null )");
                sc.add("{");
                if (domAsXpp3) {
                    sc.addIndented(copyField
                            + " = new org.codehaus.plexus.util.xml.Xpp3Dom( (org.codehaus.plexus.util.xml.Xpp3Dom) "
                            + thisField + " );");
                } else {
                    sc.addIndented(copyField + " = ( (org.w3c.dom.Node) " + thisField + ").cloneNode( true );");
                }
                sc.add("}");
                sc.add("");
            } else if ("Date".equalsIgnoreCase(modelField.getType()) || "java.util.Date".equals(modelField.getType())) {
                sc.add("if ( " + thisField + " != null )");
                sc.add("{");
                sc.addIndented(copyField + " = (java.util.Date) " + thisField + ".clone();");
                sc.add("}");
                sc.add("");
            } else if (ModelDefault.PROPERTIES.equals(modelField.getType())) {
                sc.add("if ( " + thisField + " != null )");
                sc.add("{");
                sc.addIndented(copyField + " = (" + ModelDefault.PROPERTIES + ") " + thisField + ".clone();");
                sc.add("}");
                sc.add("");
            } else if (modelField instanceof ModelAssociation) {
                ModelAssociation modelAssociation = (ModelAssociation) modelField;

                String cloneModeAssoc = getCloneMode(modelAssociation, cloneModeClass);

                boolean deepClone = JavaAssociationMetadata.CLONE_DEEP.equals(cloneModeAssoc)
                        && !immutableTypes.contains(modelAssociation.getTo());

                if (modelAssociation.isOneMultiplicity()) {
                    if (deepClone) {
                        sc.add("if ( " + thisField + " != null )");
                        sc.add("{");
                        sc.addIndented(copyField + " = (" + modelAssociation.getTo() + ") " + thisField + ".clone();");
                        sc.add("}");
                        sc.add("");
                    }
                } else {
                    sc.add("if ( " + thisField + " != null )");
                    sc.add("{");
                    sc.indent();

                    JavaAssociationMetadata javaAssociationMetadata = getJavaAssociationMetadata(modelAssociation);
                    JType componentType = getComponentType(modelAssociation, javaAssociationMetadata);

                    sc.add(copyField + " = " + getDefaultValue(modelAssociation, componentType) + ";");

                    if (isCollection(modelField.getType())) {
                        if (deepClone) {
                            sc.add("for ( " + componentType.getName() + " item : " + thisField + " )");
                            sc.add("{");
                            sc.indent();
                            sc.add(copyField + ".add( ( (" + modelAssociation.getTo() + ") item).clone() );");
                            sc.unindent();
                            sc.add("}");
                        } else {
                            sc.add(copyField + ".addAll( " + thisField + " );");
                        }
                    } else if (isMap(modelField.getType())) {
                        sc.add(copyField + ".clear();");
                        sc.add(copyField + ".putAll( " + thisField + " );");
                    }

                    sc.unindent();
                    sc.add("}");
                    sc.add("");
                }
            }
        }

        if (locationClass != null) {
            String locationField =
                    ((ModelClassMetadata) locationClass.getMetadata(ModelClassMetadata.ID)).getLocationTracker();
            sc.add("if ( copy." + locationField + " != null )");
            sc.add("{");
            sc.indent();
            sc.add("copy." + locationField + " = new java.util.LinkedHashMap" + "( copy." + locationField + " );");
            sc.unindent();
            sc.add("}");
            sc.add("");
        }

        String cloneHook = getCloneHook(modelClass);

        if (StringUtils.isNotEmpty(cloneHook) && !"false".equalsIgnoreCase(cloneHook)) {
            if ("true".equalsIgnoreCase(cloneHook)) {
                cloneHook = "cloneHook";
            }

            sc.add(cloneHook + "( copy );");
            sc.add("");
        }

        sc.add("return copy;");

        sc.unindent();
        sc.add("}");
        sc.add("catch ( " + Exception.class.getName() + " ex )");
        sc.add("{");
        sc.indent();
        sc.add("throw (" + RuntimeException.class.getName() + ") new " + UnsupportedOperationException.class.getName()
                + "( getClass().getName()");
        sc.addIndented("+ \" does not support clone()\" ).initCause( ex );");
        sc.unindent();
        sc.add("}");

        return new JMethod[] {cloneMethod};
    }

    private String getCloneMode(ModelClass modelClass) throws ModelloException {
        String cloneMode = null;

        for (ModelClass currentClass = modelClass; ; ) {
            JavaClassMetadata javaClassMetadata = (JavaClassMetadata) currentClass.getMetadata(JavaClassMetadata.ID);

            cloneMode = javaClassMetadata.getCloneMode();

            if (cloneMode != null) {
                break;
            }

            String superClass = currentClass.getSuperClass();
            if (StringUtils.isEmpty(superClass) || !isClassInModel(superClass, getModel())) {
                break;
            }

            currentClass = getModel().getClass(superClass, getGeneratedVersion());
        }

        if (cloneMode == null) {
            cloneMode = JavaClassMetadata.CLONE_NONE;
        } else if (!JavaClassMetadata.CLONE_MODES.contains(cloneMode)) {
            throw new ModelloException("The Java Modello Generator cannot use '" + cloneMode
                    + "' as a value for <class java.clone=\"...\">, " + "only the following values are acceptable "
                    + JavaClassMetadata.CLONE_MODES);
        }

        return cloneMode;
    }

    private String getCloneMode(ModelAssociation modelAssociation, String cloneModeClass) throws ModelloException {
        JavaAssociationMetadata javaAssociationMetadata =
                (JavaAssociationMetadata) modelAssociation.getAssociationMetadata(JavaAssociationMetadata.ID);

        String cloneModeAssoc = javaAssociationMetadata.getCloneMode();
        if (cloneModeAssoc == null) {
            cloneModeAssoc = cloneModeClass;
        } else if (!JavaAssociationMetadata.CLONE_MODES.contains(cloneModeAssoc)) {
            throw new ModelloException("The Java Modello Generator cannot use '" + cloneModeAssoc
                    + "' as a value for <association java.clone=\"...\">, "
                    + "only the following values are acceptable "
                    + JavaAssociationMetadata.CLONE_MODES);
        }

        return cloneModeAssoc;
    }

    private String getCloneHook(ModelClass modelClass) throws ModelloException {
        JavaClassMetadata javaClassMetadata = (JavaClassMetadata) modelClass.getMetadata(JavaClassMetadata.ID);

        return javaClassMetadata.getCloneHook();
    }

    private String generateLocationTracker(Model objectModel, ModelClass locationClass)
            throws ModelloException, IOException {
        if (locationClass == null) {
            return null;
        }

        String locationField =
                ((ModelClassMetadata) locationClass.getMetadata(ModelClassMetadata.ID)).getLocationTracker();

        String propertyName = capitalise(singular(locationField));

        String interfaceName = locationClass.getName() + "Tracker";

        String packageName = locationClass.getPackageName(isPackageWithVersion(), getGeneratedVersion());

        JSourceWriter sourceWriter = newJSourceWriter(packageName, interfaceName);

        JInterface jInterface = new JInterface(packageName + '.' + interfaceName);

        initHeader(jInterface);

        suppressAllWarnings(objectModel, jInterface);

        JMethodSignature jMethod = new JMethodSignature("get" + propertyName, new JType(locationClass.getName()));
        jMethod.setComment("Gets the location of the specified field in the input source.");
        addParameter(jMethod, "Object", "field", "The key of the field, must not be <code>null</code>.");
        String returnDoc = "The location of the field in the input source or <code>null</code> if unknown.";
        jMethod.getJDocComment().addDescriptor(JDocDescriptor.createReturnDesc(returnDoc));
        jInterface.addMethod(jMethod);

        jMethod = new JMethodSignature("set" + propertyName, null);
        jMethod.setComment("Sets the location of the specified field.");
        addParameter(jMethod, "Object", "field", "The key of the field, must not be <code>null</code>.");
        addParameter(
                jMethod,
                locationClass.getName(),
                singular(locationField),
                "The location of the field, may be <code>null</code>.");
        jInterface.addMethod(jMethod);

        jInterface.print(sourceWriter);

        sourceWriter.close();

        return jInterface.getName();
    }

    private void generateLocationTracking(JClass jClass, ModelClass modelClass, ModelClass locationClass)
            throws ModelloException {
        if (locationClass == null) {
            return;
        }

        String superClass = modelClass.getSuperClass();
        ModelClassMetadata metadata = (ModelClassMetadata) locationClass.getMetadata(ModelClassMetadata.ID);
        String locationField = metadata.getLocationTracker();
        boolean hasModeSuperClass = StringUtils.isNotEmpty(superClass) && isClassInModel(superClass, getModel());
        if (!hasModeSuperClass) {
            String fieldType = "java.util.Map<Object, " + locationClass.getName() + ">";
            String fieldImpl = "java.util.LinkedHashMap<Object, " + locationClass.getName() + ">";

            // private java.util.Map<Object, Location> locations;
            JField jField = new JField(new JType(fieldType), locationField);
            jClass.addField(jField);

            // public Location getOtherLocation( Object key )
            JMethod getter = new JMethod(
                    "getOther" + capitalise(singular(locationField)), new JType(locationClass.getName()), null);
            getter.addParameter(new JParameter(new JType("Object"), "key"));
            getter.getModifiers().makePrivate();
            JSourceCode getterSc = getter.getSourceCode();
            getterSc.add("return ( " + locationField + " != null ) ? " + locationField + ".get( key ) : null;");
            getter.setComment("");
            jClass.addMethod(getter);

            // public void setOtherLocation( Object key, Location location )
            JMethod setter = new JMethod("setOther" + capitalise(singular(locationField)));
            setter.addParameter(new JParameter(new JType("Object"), "key"));
            setter.addParameter(new JParameter(new JType(locationClass.getName()), singular(locationField)));
            JSourceCode setterSc = setter.getSourceCode();
            setterSc.add("if ( " + singular(locationField) + " != null )");
            setterSc.add("{");
            setterSc.indent();
            setterSc.add("if ( this." + locationField + " == null )");
            setterSc.add("{");
            setterSc.addIndented("this." + locationField + " = new " + fieldImpl + "();");
            setterSc.add("}");
            setterSc.add("this." + locationField + ".put( key, " + singular(locationField) + " );");
            setterSc.unindent();
            setterSc.add("}");
            setter.setComment("");
            jClass.addMethod(setter);
        }

        JField ownLocation = new JField(new JType(locationClass.getName()), singular(locationField));
        jClass.addField(ownLocation);
        for (ModelField field : modelClass.getAllFields()) {
            JField fieldLocation = new JField(
                    new JType(locationClass.getName()), field.getName() + capitalise(singular(locationField)));
            jClass.addField(fieldLocation);
        }

        // public Location getLocation( Object key )
        JMethod getter =
                new JMethod("get" + capitalise(singular(locationField)), new JType(locationClass.getName()), null);
        getter.addParameter(new JParameter(new JType("Object"), "key"));
        JSourceCode getterSc = getter.getSourceCode();

        getterSc.add("if ( key instanceof String )");
        getterSc.add("{");
        getterSc.indent();
        getterSc.add("switch ( ( String ) key )");
        getterSc.add("{");
        getterSc.indent();
        getterSc.add("case \"\" :");
        getterSc.add("{");
        getterSc.indent();
        getterSc.add("return this." + singular(locationField) + ";");
        getterSc.unindent();
        getterSc.add("}");
        for (ModelField field : modelClass.getAllFields()) {
            getterSc.add("case \"" + field.getName() + "\" :");
            getterSc.add("{");
            getterSc.indent();
            getterSc.add("return " + field.getName() + capitalise(singular(locationField)) + ";");
            getterSc.unindent();
            getterSc.add("}");
        }
        getterSc.add("default :");
        getterSc.add("{");
        getterSc.indent();
        if (hasModeSuperClass) {
            getterSc.add("return super.get" + capitalise(singular(locationField)) + "( key );");
        } else {
            getterSc.add("return getOther" + capitalise(singular(locationField)) + "( key );");
        }
        getterSc.unindent();
        getterSc.add("}");
        getterSc.add("}");
        getterSc.unindent();
        getterSc.add("}");
        getterSc.add("else");
        getterSc.add("{");
        getterSc.indent();
        if (hasModeSuperClass) {
            getterSc.add("return super.get" + capitalise(singular(locationField)) + "( key );");
        } else {
            getterSc.add("return getOther" + capitalise(singular(locationField)) + "( key );");
        }
        getterSc.unindent();
        getterSc.add("}");

        getter.setComment("");
        jClass.addMethod(getter);

        // public void setLocation( Object key, Location location )
        JMethod setter = new JMethod("set" + capitalise(singular(locationField)));
        setter.addParameter(new JParameter(new JType("Object"), "key"));
        setter.addParameter(new JParameter(new JType(locationClass.getName()), singular(locationField)));
        JSourceCode setterSc = setter.getSourceCode();
        setterSc.add("if ( key instanceof String )");
        setterSc.add("{");
        setterSc.indent();
        setterSc.add("switch ( ( String ) key )");
        setterSc.add("{");
        setterSc.indent();
        setterSc.add("case \"\" :");
        setterSc.add("{");
        setterSc.indent();
        setterSc.add("this." + singular(locationField) + " = " + singular(locationField) + ";");
        setterSc.add("return;");
        setterSc.unindent();
        setterSc.add("}");
        for (ModelField field : modelClass.getAllFields()) {
            setterSc.add("case \"" + field.getName() + "\" :");
            setterSc.add("{");
            setterSc.indent();
            setterSc.add(field.getName() + capitalise(singular(locationField)) + " = " + singular(locationField) + ";");
            setterSc.add("return;");
            setterSc.unindent();
            setterSc.add("}");
        }
        setterSc.add("default :");
        setterSc.add("{");
        setterSc.indent();
        if (hasModeSuperClass) {
            setterSc.add(
                    "super.set" + capitalise(singular(locationField)) + "( key, " + singular(locationField) + " );");
        } else {
            setterSc.add(
                    "setOther" + capitalise(singular(locationField)) + "( key, " + singular(locationField) + " );");
        }
        setterSc.add("return;");
        setterSc.unindent();
        setterSc.add("}");
        setterSc.unindent();
        setterSc.add("}");

        setterSc.unindent();
        setterSc.add("}");
        setterSc.add("else");
        setterSc.add("{");
        setterSc.indent();
        if (hasModeSuperClass) {
            setterSc.add(
                    "super.set" + capitalise(singular(locationField)) + "( key, " + singular(locationField) + " );");
        } else {
            setterSc.add(
                    "setOther" + capitalise(singular(locationField)) + "( key, " + singular(locationField) + " );");
        }
        setterSc.unindent();
        setterSc.add("}");

        setter.setComment("");
        jClass.addMethod(setter);
    }

    private void generateLocationBean(JClass jClass, ModelClass locationClass, ModelClass sourceClass)
            throws ModelloException {
        jClass.getModifiers().setFinal(true);

        String locationsField =
                ((ModelClassMetadata) locationClass.getMetadata(ModelClassMetadata.ID)).getLocationTracker();

        JavaFieldMetadata readOnlyField = new JavaFieldMetadata();
        readOnlyField.setSetter(false);

        // int lineNumber;
        ModelField lineNumber = new ModelField(locationClass, "lineNumber");
        lineNumber.setDescription("The one-based line number. The value will be non-positive if unknown.");
        lineNumber.setType("int");
        lineNumber.setDefaultValue("-1");
        lineNumber.addMetadata(readOnlyField);
        createField(jClass, lineNumber);

        // int columnNumber;
        ModelField columnNumber = new ModelField(locationClass, "columnNumber");
        columnNumber.setDescription("The one-based column number. The value will be non-positive if unknown.");
        columnNumber.setType("int");
        columnNumber.setDefaultValue("-1");
        columnNumber.addMetadata(readOnlyField);
        createField(jClass, columnNumber);

        // Source source;
        ModelField source = null;
        if (sourceClass != null) {
            ModelClassMetadata metadata = (ModelClassMetadata) sourceClass.getMetadata(ModelClassMetadata.ID);
            String sourceField = metadata.getSourceTracker();

            source = new ModelField(locationClass, sourceField);
            source.setType(sourceClass.getName());
            source.addMetadata(readOnlyField);
            createField(jClass, source);
        }

        // Location( int lineNumber, int columnNumber );
        JConstructor jConstructor = jClass.createConstructor();
        JSourceCode sc = jConstructor.getSourceCode();

        jConstructor.addParameter(new JParameter(JType.INT, lineNumber.getName()));
        sc.add("this." + lineNumber.getName() + " = " + lineNumber.getName() + ";");

        jConstructor.addParameter(new JParameter(JType.INT, columnNumber.getName()));
        sc.add("this." + columnNumber.getName() + " = " + columnNumber.getName() + ";");

        // Location( int lineNumber, int columnNumber, Source source );
        if (sourceClass != null) {
            jConstructor = jClass.createConstructor(jConstructor.getParameters());
            sc.copyInto(jConstructor.getSourceCode());
            sc = jConstructor.getSourceCode();

            jConstructor.addParameter(new JParameter(new JType(sourceClass.getName()), source.getName()));
            sc.add("this." + source.getName() + " = " + source.getName() + ";");
        }

        JType fieldType = new JMapType("java.util.Map", new JType(locationClass.getName()));
        JType fieldImpl = new JMapType("java.util.LinkedHashMap", new JType(locationClass.getName()));

        // public Map<Object, Location> getLocations()
        JMethod jMethod = new JMethod("get" + capitalise(locationsField), fieldType, null);
        sc = jMethod.getSourceCode();
        sc.add("return " + locationsField + ";");
        jMethod.setComment("");
        jClass.addMethod(jMethod);

        // public void setLocations( Map<Object, Location> locations )
        jMethod = new JMethod("set" + capitalise(locationsField));
        jMethod.addParameter(new JParameter(fieldType, locationsField));
        sc = jMethod.getSourceCode();
        sc.add("this." + locationsField + " = " + locationsField + ";");
        jMethod.setComment("");
        jClass.addMethod(jMethod);

        // public static Location merge( Location target, Location source, boolean sourceDominant )
        jMethod = new JMethod("merge", new JType(locationClass.getName()), null);
        jMethod.getModifiers().setStatic(true);
        jMethod.addParameter(new JParameter(new JType(locationClass.getName()), "target"));
        jMethod.addParameter(new JParameter(new JType(locationClass.getName()), "source"));
        jMethod.addParameter(new JParameter(JType.BOOLEAN, "sourceDominant"));
        sc = jMethod.getSourceCode();
        sc.add("if ( source == null )");
        sc.add("{");
        sc.addIndented("return target;");
        sc.add("}");
        sc.add("else if ( target == null )");
        sc.add("{");
        sc.addIndented("return source;");
        sc.add("}");
        sc.add("");
        sc.add(locationClass.getName() + " result =");
        sc.add("    new " + locationClass.getName() + "( target.getLineNumber(), target.getColumnNumber()"
                + (sourceClass != null ? ", target.get" + capitalise(source.getName()) + "()" : "") + " );");
        sc.add("");
        sc.add(fieldType + " locations;");
        sc.add(fieldType + " sourceLocations = source.get" + capitalise(locationsField) + "();");
        sc.add(fieldType + " targetLocations = target.get" + capitalise(locationsField) + "();");
        sc.add("if ( sourceLocations == null )");
        sc.add("{");
        sc.addIndented("locations = targetLocations;");
        sc.add("}");
        sc.add("else if ( targetLocations == null )");
        sc.add("{");
        sc.addIndented("locations = sourceLocations;");
        sc.add("}");
        sc.add("else");
        sc.add("{");
        sc.addIndented("locations = new " + fieldImpl.getName() + "();");
        sc.addIndented("locations.putAll( sourceDominant ? targetLocations : sourceLocations );");
        sc.addIndented("locations.putAll( sourceDominant ? sourceLocations : targetLocations );");
        sc.add("}");
        sc.add("result.set" + capitalise(locationsField) + "( locations );");
        sc.add("");
        sc.add("return result;");
        jClass.addMethod(jMethod);

        // public static Location merge( Location target, Location source, Collection<Integer> indices )
        jMethod = new JMethod("merge", new JType(locationClass.getName()), null);
        jMethod.getModifiers().setStatic(true);
        jMethod.addParameter(new JParameter(new JType(locationClass.getName()), "target"));
        jMethod.addParameter(new JParameter(new JType(locationClass.getName()), "source"));
        jMethod.addParameter(
                new JParameter(new JCollectionType("java.util.Collection", new JType("Integer")), "indices"));
        sc = jMethod.getSourceCode();
        sc.add("if ( source == null )");
        sc.add("{");
        sc.addIndented("return target;");
        sc.add("}");
        sc.add("else if ( target == null )");
        sc.add("{");
        sc.addIndented("return source;");
        sc.add("}");
        sc.add("");
        sc.add(locationClass.getName() + " result =");
        sc.add("    new " + locationClass.getName() + "( target.getLineNumber(), target.getColumnNumber()"
                + (sourceClass != null ? ", target.get" + capitalise(source.getName()) + "()" : "") + " );");
        sc.add("");
        sc.add(fieldType + " locations;");
        sc.add(fieldType + " sourceLocations = source.get" + capitalise(locationsField) + "();");
        sc.add(fieldType + " targetLocations = target.get" + capitalise(locationsField) + "();");
        sc.add("if ( sourceLocations == null )");
        sc.add("{");
        sc.addIndented("locations = targetLocations;");
        sc.add("}");
        sc.add("else if ( targetLocations == null )");
        sc.add("{");
        sc.addIndented("locations = sourceLocations;");
        sc.add("}");
        sc.add("else");
        sc.add("{");
        sc.indent();
        sc.add("locations = new " + fieldImpl + "();");
        sc.add("for ( java.util.Iterator<Integer> it = indices.iterator(); it.hasNext(); )");
        sc.add("{");
        sc.indent();
        sc.add(locationClass.getName() + " location;");
        sc.add("Integer index = it.next();");
        sc.add("if ( index.intValue() < 0 )");
        sc.add("{");
        sc.addIndented("location = sourceLocations.get(Integer.valueOf(~index.intValue()));");
        sc.add("}");
        sc.add("else");
        sc.add("{");
        sc.addIndented("location = targetLocations.get( index );");
        sc.add("}");
        sc.add("locations.put(Integer.valueOf(locations.size()), location);");
        sc.unindent();
        sc.add("}");
        sc.unindent();
        sc.add("}");
        sc.add("result.set" + capitalise(locationsField) + "( locations );");
        sc.add("");
        sc.add("return result;");
        jClass.addMethod(jMethod);

        JClass stringFormatterClass = jClass.createInnerClass("StringFormatter");
        stringFormatterClass.getModifiers().setStatic(true);
        stringFormatterClass.getModifiers().setAbstract(true);

        jMethod = new JMethod("toString", new JType("String"), null);
        jMethod.getModifiers().setAbstract(true);
        jMethod.addParameter(new JParameter(new JType(locationClass.getName()), "location"));
        stringFormatterClass.addMethod(jMethod);
    }

    /**
     * Utility method that adds a period to the end of a string, if the last non-whitespace character of the string is
     * not a punctuation mark or an end-tag.
     *
     * @param string The string to work with
     * @return The string that came in but with a period at the end
     */
    private String appendPeriod(String string) {
        if (string == null) {
            return string;
        }

        String trimmedString = string.trim();
        if (trimmedString.endsWith(".")
                || trimmedString.endsWith("!")
                || trimmedString.endsWith("?")
                || trimmedString.endsWith(">")) {
            return string;
        } else {
            return string + ".";
        }
    }

    private String createHashCodeForField(ModelField identifier) {
        String name = identifier.getName();
        String type = identifier.getType();

        if ("boolean".equals(type)) {
            return "( " + name + " ? 0 : 1 )";
        } else if ("byte".equals(type) || "char".equals(type) || "short".equals(type) || "int".equals(type)) {
            return "(int) " + name;
        } else if ("long".equals(type)) {
            return "(int) ( " + name + " ^ ( " + name + " >>> 32 ) )";
        } else if ("float".equals(type)) {
            return "Float.floatToIntBits( " + name + " )";
        } else if ("double".equals(type)) {
            return "(int) ( Double.doubleToLongBits( " + identifier.getName() + " ) ^ ( Double.doubleToLongBits( "
                    + identifier.getName() + " ) >>> 32 ) )";
        } else {
            return "( " + name + " != null ? " + name + ".hashCode() : 0 )";
        }
    }

    private JField createField(ModelField modelField) throws ModelloException {
        JType type;

        String baseType = modelField.getType();
        if (modelField.isArray()) {
            // remove [] at the end of the type
            baseType = baseType.substring(0, baseType.length() - 2);
        }

        if ("boolean".equals(baseType)) {
            type = JType.BOOLEAN;
        } else if ("byte".equals(baseType)) {
            type = JType.BYTE;
        } else if ("char".equals(baseType)) {
            type = JType.CHAR;
        } else if ("double".equals(baseType)) {
            type = JType.DOUBLE;
        } else if ("float".equals(baseType)) {
            type = JType.FLOAT;
        } else if ("int".equals(baseType)) {
            type = JType.INT;
        } else if ("short".equals(baseType)) {
            type = JType.SHORT;
        } else if ("long".equals(baseType)) {
            type = JType.LONG;
        } else if ("Date".equals(baseType)) {
            type = new JClass("java.util.Date");
        } else if ("DOM".equals(baseType)) {
            // TODO: maybe DOM is not how to specify it in the model, but just Object and markup Xpp3Dom for the
            // Xpp3Reader?
            // not sure how we'll treat it for the other sources, eg sql.
            type = new JClass("Object");
        } else {
            type = new JClass(baseType);
        }

        if (modelField.isArray()) {
            type = new JArrayType(type);
        }

        JField field = new JField(type, modelField.getName());

        if (modelField.isModelVersionField()) {
            field.setInitString("\"" + getGeneratedVersion() + "\"");
        }

        if (modelField.getDefaultValue() != null) {
            field.setInitString(getJavaDefaultValue(modelField));
        }

        if (StringUtils.isNotEmpty(modelField.getDescription())) {
            field.setComment(appendPeriod(modelField.getDescription()));
        }

        if (!modelField.getAnnotations().isEmpty()) {
            for (String annotation : modelField.getAnnotations()) {
                field.appendAnnotation(annotation);
            }
        }

        return field;
    }

    private void createField(JClass jClass, ModelField modelField) throws ModelloException {
        JavaFieldMetadata javaFieldMetadata = (JavaFieldMetadata) modelField.getMetadata(JavaFieldMetadata.ID);

        JField field = createField(modelField);

        jClass.addField(field);

        if (javaFieldMetadata.isGetter()) {
            jClass.addMethod(createGetter(field, modelField));
        }

        if (javaFieldMetadata.isSetter()) {
            jClass.addMethod(createSetter(field, modelField));
        }
    }

    private JMethod createGetter(JField field, ModelField modelField) {
        String propertyName = capitalise(field.getName());

        JavaFieldMetadata javaFieldMetadata = (JavaFieldMetadata) modelField.getMetadata(JavaFieldMetadata.ID);

        String prefix = javaFieldMetadata.isBooleanGetter() ? "is" : "get";

        JType returnType = field.getType();
        String interfaceCast = "";

        if (modelField instanceof ModelAssociation) {
            ModelAssociation modelAssociation = (ModelAssociation) modelField;

            JavaAssociationMetadata javaAssociationMetadata =
                    (JavaAssociationMetadata) modelAssociation.getAssociationMetadata(JavaAssociationMetadata.ID);

            if (StringUtils.isNotEmpty(javaAssociationMetadata.getInterfaceName())
                    && !javaFieldMetadata.isBooleanGetter()) {
                returnType = new JClass(javaAssociationMetadata.getInterfaceName());

                interfaceCast = "(" + javaAssociationMetadata.getInterfaceName() + ") ";
            }
        }

        JMethod getter = new JMethod(prefix + propertyName, returnType, null);

        StringBuilder comment = new StringBuilder("Get ");
        if (StringUtils.isEmpty(modelField.getDescription())) {
            comment.append("the ");
            comment.append(field.getName());
            comment.append(" field");
        } else {
            comment.append(
                    StringUtils.lowercaseFirstLetter(modelField.getDescription().trim()));
        }
        getter.getJDocComment().setComment(appendPeriod(comment.toString()));

        getter.getSourceCode().add("return " + interfaceCast + "this." + field.getName() + ";");

        return getter;
    }

    private JMethod createSetter(JField field, ModelField modelField) throws ModelloException {
        return createSetter(field, modelField, false);
    }

    // since 1.8
    private JMethod createSetter(JField field, ModelField modelField, boolean isBuilderMethod) throws ModelloException {
        String propertyName = capitalise(field.getName());

        JMethod setter;
        if (isBuilderMethod) {
            setter = new JMethod("set" + propertyName, new JClass("Builder"), "this builder instance");
        } else {
            setter = new JMethod("set" + propertyName);
        }

        StringBuilder comment = new StringBuilder("Set ");
        if (StringUtils.isEmpty(modelField.getDescription())) {
            comment.append("the ");
            comment.append(field.getName());
            comment.append(" field");
        } else {
            comment.append(
                    StringUtils.lowercaseFirstLetter(modelField.getDescription().trim()));
        }
        setter.getJDocComment().setComment(appendPeriod(comment.toString()));

        JType parameterType = getDesiredType(modelField, false);

        setter.addParameter(new JParameter(parameterType, field.getName()));

        JSourceCode sc = setter.getSourceCode();

        if (modelField instanceof ModelAssociation) {
            ModelAssociation modelAssociation = (ModelAssociation) modelField;

            JavaAssociationMetadata javaAssociationMetadata =
                    (JavaAssociationMetadata) modelAssociation.getAssociationMetadata(JavaAssociationMetadata.ID);

            boolean isOneMultiplicity =
                    isBidirectionalAssociation(modelAssociation) && modelAssociation.isOneMultiplicity();

            if (isOneMultiplicity && javaAssociationMetadata.isBidi()) {
                sc.add("if ( this." + field.getName() + " != null )");

                sc.add("{");

                sc.indent();

                sc.add("this." + field.getName() + ".break"
                        + modelAssociation.getModelClass().getName() + "Association( this );");

                sc.unindent();

                sc.add("}");

                sc.add("");
            }

            String interfaceCast = "";

            if (StringUtils.isNotEmpty(javaAssociationMetadata.getInterfaceName())
                    && modelAssociation.isOneMultiplicity()) {
                interfaceCast = "(" + field.getType().getName() + ") ";

                createClassCastAssertion(sc, modelAssociation, "set");
            }

            sc.add("this." + field.getName() + " = " + interfaceCast + field.getName() + ";");

            if (isOneMultiplicity && javaAssociationMetadata.isBidi()) {
                sc.add("");

                sc.add("if ( " + field.getName() + " != null )");

                sc.add("{");

                sc.indent();

                sc.add("this." + field.getName() + ".create"
                        + modelAssociation.getModelClass().getName() + "Association( this );");

                sc.unindent();

                sc.add("}");
            }
        } else {
            sc.add("this." + field.getName() + " = " + field.getName() + ";");
        }

        if (isBuilderMethod) {
            sc.add("return this;");
        }

        return setter;
    }

    private void createClassCastAssertion(JSourceCode sc, ModelAssociation modelAssociation, String crudModifier)
            throws ModelloException {
        JavaAssociationMetadata javaAssociationMetadata =
                (JavaAssociationMetadata) modelAssociation.getAssociationMetadata(JavaAssociationMetadata.ID);

        if (StringUtils.isEmpty(javaAssociationMetadata.getInterfaceName())) {
            return; // java.useInterface feature not used, no class cast assertion needed
        }

        String propertyName = capitalise(modelAssociation.getName());

        JField field = createField(modelAssociation);
        String fieldName = field.getName();
        JType type = new JClass(modelAssociation.getTo());

        if (modelAssociation.isManyMultiplicity()) {
            fieldName = uncapitalise(modelAssociation.getTo());
        }

        String instanceName = type.getName();

        // Add sane class cast exception message
        // When will sun ever fix this?

        sc.add("if ( " + fieldName + " != null && !( " + fieldName + " instanceof " + instanceName + " ) )");

        sc.add("{");

        sc.indent();

        sc.add("throw new ClassCastException( \""
                + modelAssociation.getModelClass().getName() + "." + crudModifier
                + propertyName + "( " + fieldName + " ) parameter must be instanceof \" + " + instanceName
                + ".class.getName() );");

        sc.unindent();

        sc.add("}");
    }

    private void createAssociation(JClass jClass, ModelAssociation modelAssociation, JSourceCode jConstructorSource)
            throws ModelloException {
        JavaFieldMetadata javaFieldMetadata = (JavaFieldMetadata) modelAssociation.getMetadata(JavaFieldMetadata.ID);

        JavaAssociationMetadata javaAssociationMetadata = getJavaAssociationMetadata(modelAssociation);

        if (modelAssociation.isManyMultiplicity()) {
            JType componentType = getComponentType(modelAssociation, javaAssociationMetadata);

            String defaultValue = getDefaultValue(modelAssociation, componentType);

            JType type;
            if (modelAssociation.isGenericType()) {
                type = new JCollectionType(modelAssociation.getType(), componentType);
            } else if (ModelDefault.MAP.equals(modelAssociation.getType())) {
                JMapType mapType = new JMapType(modelAssociation.getType(), defaultValue, componentType);
                defaultValue = mapType.getInstanceName();
                type = mapType;
            } else {
                type = new JClass(modelAssociation.getType());
            }

            JField jField = new JField(type, modelAssociation.getName());

            if (!isEmpty(modelAssociation.getComment())) {
                jField.setComment(modelAssociation.getComment());
            }

            if (!modelAssociation.getAnnotations().isEmpty()) {
                for (String annotation : modelAssociation.getAnnotations()) {
                    jField.appendAnnotation(annotation);
                }
            }

            if (Objects.equals(javaAssociationMetadata.getInitializationMode(), JavaAssociationMetadata.FIELD_INIT)) {
                jField.setInitString(defaultValue);
            }

            if (Objects.equals(
                    javaAssociationMetadata.getInitializationMode(), JavaAssociationMetadata.CONSTRUCTOR_INIT)) {
                jConstructorSource.add("this." + jField.getName() + " = " + defaultValue + ";");
            }

            jClass.addField(jField);

            if (javaFieldMetadata.isGetter()) {
                String propertyName = capitalise(jField.getName());

                JMethod getter = new JMethod("get" + propertyName, jField.getType(), null);

                JSourceCode sc = getter.getSourceCode();

                if (Objects.equals(
                        javaAssociationMetadata.getInitializationMode(), JavaAssociationMetadata.LAZY_INIT)) {
                    sc.add("if ( this." + jField.getName() + " == null )");

                    sc.add("{");

                    sc.indent();

                    sc.add("this." + jField.getName() + " = " + defaultValue + ";");

                    sc.unindent();

                    sc.add("}");

                    sc.add("");
                }

                sc.add("return this." + jField.getName() + ";");

                jClass.addMethod(getter);
            }

            if (javaFieldMetadata.isSetter()) {
                jClass.addMethod(createSetter(jField, modelAssociation));
            }

            if (javaAssociationMetadata.isAdder()) {
                createAdder(modelAssociation, jClass);
            }
        } else {
            createField(jClass, modelAssociation);
        }

        if (isBidirectionalAssociation(modelAssociation)) {
            if (javaAssociationMetadata.isBidi()) {
                createCreateAssociation(jClass, modelAssociation);
                createBreakAssociation(jClass, modelAssociation);
            }
        }
    }

    private String getDefaultValue(ModelAssociation modelAssociation, JType componentType) {
        String defaultValue = getDefaultValue(modelAssociation);

        if (modelAssociation.isGenericType()) {
            ModelDefault modelDefault = getModel().getDefault(modelAssociation.getType());
            defaultValue = StringUtils.replace(modelDefault.getValue(), "<?>", "<" + componentType.getName() + ">");
        }
        return defaultValue;
    }

    private JavaAssociationMetadata getJavaAssociationMetadata(ModelAssociation modelAssociation)
            throws ModelloException {
        JavaAssociationMetadata javaAssociationMetadata =
                (JavaAssociationMetadata) modelAssociation.getAssociationMetadata(JavaAssociationMetadata.ID);

        if (!JavaAssociationMetadata.INIT_TYPES.contains(javaAssociationMetadata.getInitializationMode())) {
            throw new ModelloException("The Java Modello Generator cannot use '"
                    + javaAssociationMetadata.getInitializationMode()
                    + "' as a <association java.init=\"" + javaAssociationMetadata.getInitializationMode() + "\"> "
                    + "value, the only the following are acceptable " + JavaAssociationMetadata.INIT_TYPES);
        }
        return javaAssociationMetadata;
    }

    private JType getComponentType(ModelAssociation modelAssociation, JavaAssociationMetadata javaAssociationMetadata) {
        JType componentType;
        if (javaAssociationMetadata.getInterfaceName() != null) {
            componentType = new JClass(javaAssociationMetadata.getInterfaceName());
        } else {
            componentType = new JClass(modelAssociation.getTo());
        }
        return componentType;
    }

    private void createCreateAssociation(JClass jClass, ModelAssociation modelAssociation) {
        JMethod createMethod = new JMethod("create" + modelAssociation.getTo() + "Association");

        JavaAssociationMetadata javaAssociationMetadata =
                (JavaAssociationMetadata) modelAssociation.getAssociationMetadata(JavaAssociationMetadata.ID);

        createMethod.addParameter(
                new JParameter(new JClass(modelAssociation.getTo()), uncapitalise(modelAssociation.getTo())));

        // TODO: remove after tested
        // createMethod.addException( new JClass( "Exception" ) );

        JSourceCode sc = createMethod.getSourceCode();

        if (modelAssociation.isOneMultiplicity()) {
            if (javaAssociationMetadata.isBidi()) {
                sc.add("if ( this." + modelAssociation.getName() + " != null )");

                sc.add("{");

                sc.indent();

                sc.add("break" + modelAssociation.getTo() + "Association( this." + modelAssociation.getName() + " );");

                sc.unindent();

                sc.add("}");

                sc.add("");
            }

            sc.add("this." + modelAssociation.getName() + " = " + uncapitalise(modelAssociation.getTo()) + ";");
        } else {
            jClass.addImport("java.util.Collection");

            sc.add("Collection " + modelAssociation.getName() + " = get" + capitalise(modelAssociation.getName())
                    + "();");

            sc.add("");

            sc.add("if ( " + modelAssociation.getName() + ".contains( " + uncapitalise(modelAssociation.getTo())
                    + " ) )");

            sc.add("{");

            sc.indent();

            sc.add("throw new IllegalStateException( \"" + uncapitalise(modelAssociation.getTo())
                    + " is already assigned.\" );");

            sc.unindent();

            sc.add("}");

            sc.add("");

            sc.add(modelAssociation.getName() + ".add( " + uncapitalise(modelAssociation.getTo()) + " );");
        }

        jClass.addMethod(createMethod);
    }

    private void createBreakAssociation(JClass jClass, ModelAssociation modelAssociation) {
        JSourceCode sc;
        JMethod breakMethod = new JMethod("break" + modelAssociation.getTo() + "Association");

        breakMethod.addParameter(
                new JParameter(new JClass(modelAssociation.getTo()), uncapitalise(modelAssociation.getTo())));

        // TODO: remove after tested
        // breakMethod.addException( new JClass( "Exception" ) );

        sc = breakMethod.getSourceCode();

        if (modelAssociation.isOneMultiplicity()) {
            sc.add("if ( this." + modelAssociation.getName() + " != " + uncapitalise(modelAssociation.getTo()) + " )");

            sc.add("{");

            sc.indent();

            sc.add("throw new IllegalStateException( \"" + uncapitalise(modelAssociation.getTo())
                    + " isn't associated.\" );");

            sc.unindent();

            sc.add("}");

            sc.add("");

            sc.add("this." + modelAssociation.getName() + " = null;");
        } else {
            JavaAssociationMetadata javaAssociationMetadata =
                    (JavaAssociationMetadata) modelAssociation.getAssociationMetadata(JavaAssociationMetadata.ID);

            String reference;

            if (JavaAssociationMetadata.LAZY_INIT.equals(javaAssociationMetadata.getInitializationMode())) {
                reference = "get" + capitalise(modelAssociation.getName()) + "()";
            } else {
                reference = modelAssociation.getName();
            }

            sc.add("if ( !" + reference + ".contains( " + uncapitalise(modelAssociation.getTo()) + " ) )");

            sc.add("{");

            sc.indent();

            sc.add("throw new IllegalStateException( \"" + uncapitalise(modelAssociation.getTo())
                    + " isn't associated.\" );");

            sc.unindent();

            sc.add("}");

            sc.add("");

            sc.add(reference + ".remove( " + uncapitalise(modelAssociation.getTo()) + " );");
        }

        jClass.addMethod(breakMethod);
    }

    private void createAdder(ModelAssociation modelAssociation, JClass jClass) throws ModelloException {
        createAdder(modelAssociation, jClass, false);
    }

    /*
     * since 1.8
     */
    private void createAdder(ModelAssociation modelAssociation, JClass jClass, boolean isBuilderMethod)
            throws ModelloException {
        String fieldName = modelAssociation.getName();

        JavaAssociationMetadata javaAssociationMetadata =
                (JavaAssociationMetadata) modelAssociation.getAssociationMetadata(JavaAssociationMetadata.ID);

        String parameterName = uncapitalise(modelAssociation.getTo());
        String implementationParameterName = parameterName;

        boolean bidirectionalAssociation = isBidirectionalAssociation(modelAssociation);

        JType addType;

        if (StringUtils.isNotEmpty(javaAssociationMetadata.getInterfaceName())) {
            addType = new JClass(javaAssociationMetadata.getInterfaceName());
            implementationParameterName = "( (" + modelAssociation.getTo() + ") " + parameterName + " )";
        } else if (modelAssociation.getToClass() != null) {
            addType = new JClass(modelAssociation.getToClass().getName());
        } else {
            addType = new JClass("String");
        }

        if (modelAssociation.getType().equals(ModelDefault.PROPERTIES)
                || modelAssociation.getType().equals(ModelDefault.MAP)) {
            String adderName = "add" + capitalise(singular(fieldName));

            JMethod adder;
            if (isBuilderMethod) {
                adder = new JMethod(adderName, new JClass("Builder"), "this builder instance");
            } else {
                adder = new JMethod(adderName);
            }

            if (modelAssociation.getType().equals(ModelDefault.MAP)) {
                adder.addParameter(new JParameter(new JClass("Object"), "key"));
            } else {
                adder.addParameter(new JParameter(new JClass("String"), "key"));
            }

            adder.addParameter(new JParameter(new JClass(modelAssociation.getTo()), "value"));

            StringBuilder adderCode = new StringBuilder();

            if (JavaAssociationMetadata.LAZY_INIT.equals(javaAssociationMetadata.getInitializationMode())
                    && !isBuilderMethod) {
                adderCode.append("get").append(capitalise(fieldName)).append("()");
            } else {
                adderCode.append(fieldName);
            }

            adderCode.append(".put( key, value );");

            adder.getSourceCode().add(adderCode.toString());

            if (isBuilderMethod) {
                adder.getSourceCode().add("return this;");
            }

            jClass.addMethod(adder);
        } else {
            String adderName = "add" + singular(capitalise(singular(fieldName)));

            JMethod adder;
            if (isBuilderMethod) {
                adder = new JMethod(adderName, new JClass("Builder"), "this builder instance");
            } else {
                adder = new JMethod(adderName);
            }

            adder.addParameter(new JParameter(addType, parameterName));

            createClassCastAssertion(adder.getSourceCode(), modelAssociation, "add");

            StringBuilder adderCode = new StringBuilder();

            if (JavaAssociationMetadata.LAZY_INIT.equals(javaAssociationMetadata.getInitializationMode())
                    && !isBuilderMethod) {
                adderCode.append("get").append(capitalise(fieldName)).append("()");
            } else {
                adderCode.append(fieldName);
            }

            adderCode.append(".add( ").append(implementationParameterName).append(" );");

            adder.getSourceCode().add(adderCode.toString());

            if (bidirectionalAssociation && javaAssociationMetadata.isBidi() && !isBuilderMethod) {
                // TODO: remove after tested
                // adder.addException( new JClass( "Exception" ) );

                adder.getSourceCode()
                        .add(implementationParameterName + ".create"
                                + modelAssociation.getModelClass().getName() + "Association( this );");
            }

            if (isBuilderMethod) {
                adder.getSourceCode().add("return this;");
            }

            jClass.addMethod(adder);

            // don't add the remover in the inner Builder class
            if (isBuilderMethod) {
                return;
            }

            JMethod remover = new JMethod("remove" + singular(capitalise(fieldName)));

            remover.addParameter(new JParameter(addType, parameterName));

            createClassCastAssertion(remover.getSourceCode(), modelAssociation, "remove");

            if (bidirectionalAssociation && javaAssociationMetadata.isBidi()) {
                // TODO: remove after tested
                // remover.addException( new JClass( "Exception" ) );

                remover.getSourceCode()
                        .add(parameterName + ".break"
                                + modelAssociation.getModelClass().getName() + "Association( this );");
            }

            String reference;

            if (JavaAssociationMetadata.LAZY_INIT.equals(javaAssociationMetadata.getInitializationMode())) {
                reference = "get" + capitalise(fieldName) + "()";
            } else {
                reference = fieldName;
            }

            remover.getSourceCode().add(reference + ".remove( " + implementationParameterName + " );");

            jClass.addMethod(remover);
        }
    }

    private boolean isBidirectionalAssociation(ModelAssociation association) {
        Model model = association.getModelClass().getModel();

        if (!isClassInModel(association.getTo(), model)) {
            return false;
        }

        ModelClass toClass = association.getToClass();

        for (ModelField modelField : toClass.getFields(getGeneratedVersion())) {
            if (!(modelField instanceof ModelAssociation)) {
                continue;
            }

            ModelAssociation modelAssociation = (ModelAssociation) modelField;

            if (association == modelAssociation) {
                continue;
            }

            if (!isClassInModel(modelAssociation.getTo(), model)) {
                continue;
            }

            ModelClass totoClass = modelAssociation.getToClass();

            if (association.getModelClass().equals(totoClass)) {
                return true;
            }
        }

        return false;
    }

    private JType getDesiredType(ModelField modelField, boolean useTo) throws ModelloException {
        JField field = createField(modelField);
        JType type = field.getType();

        if (modelField instanceof ModelAssociation) {
            ModelAssociation modelAssociation = (ModelAssociation) modelField;
            JavaAssociationMetadata javaAssociationMetadata =
                    (JavaAssociationMetadata) modelAssociation.getAssociationMetadata(JavaAssociationMetadata.ID);

            if (StringUtils.isNotEmpty(javaAssociationMetadata.getInterfaceName())
                    && !modelAssociation.isManyMultiplicity()) {
                type = new JClass(javaAssociationMetadata.getInterfaceName());
            } else if (modelAssociation.isManyMultiplicity() && modelAssociation.isGenericType()) {
                JType componentType = getComponentType(modelAssociation, javaAssociationMetadata);
                type = new JCollectionType(modelAssociation.getType(), componentType);
            } else if (useTo) {
                type = new JClass(modelAssociation.getTo());
            }
        }

        return type;
    }

    private void addParameter(JMethodSignature jMethod, String type, String name, String comment) {
        jMethod.addParameter(new JParameter(new JType(type), name));
        jMethod.getJDocComment().getParamDescriptor(name).setDescription(comment);
    }

    // ----------------------------------------------------------------------
    // Model.Builder
    // since 1.8
    // ----------------------------------------------------------------------

    private void generateBuilder(ModelClass modelClass, JClass builderClass, JConstructor outherClassConstructor)
            throws ModelloException {
        builderClass.getModifiers().setStatic(true);
        builderClass.getModifiers().setFinal(true);

        ModelClass reference = modelClass;

        // traverse the whole modelClass hierarchy to create the nested Builder
        while (reference != null) {
            // create builder setters methods
            for (ModelField modelField : reference.getFields(getGeneratedVersion())) {
                if (modelField instanceof ModelAssociation) {
                    createBuilderAssociation(builderClass, (ModelAssociation) modelField);
                } else {
                    createBuilderField(builderClass, modelField);
                }
            }

            if (reference.hasSuperClass()) {
                reference = reference.getModel().getClass(reference.getSuperClass(), getGeneratedVersion());
            } else {
                reference = null;
            }
        }

        // create and add the Model#build() method
        JMethod build = new JMethod(
                "build", new JClass(modelClass.getName()), "A new <code>" + modelClass.getName() + "</code> instance");
        build.getJDocComment().setComment("Creates a new <code>" + modelClass.getName() + "</code> instance.");

        JSourceCode sc = build.getSourceCode();

        createInstanceAndSetProperties(modelClass, outherClassConstructor, sc);

        builderClass.addMethod(build);
    }

    private void createInstanceAndSetProperties(ModelClass modelClass, JConstructor constructor, JSourceCode sc)
            throws ModelloException {
        final Set<String> ctorArgs = new HashSet<String>();

        StringBuilder ctor = new StringBuilder(modelClass.getName())
                .append(" instance = new ")
                .append(modelClass.getName())
                .append('(');

        // understand if default empty ctor can be used or if it requires parameters
        if (constructor != null) {
            JParameter[] parameters = constructor.getParameters();
            for (int i = 0; i < parameters.length; i++) {
                if (i > 0) {
                    ctor.append(',');
                }

                JParameter parameter = parameters[i];

                ctor.append(' ').append(parameter.getName()).append(' ');

                ctorArgs.add(parameter.getName());
            }
        }

        ctor.append(");");

        sc.add(ctor.toString());

        ModelClass reference = modelClass;

        // traverse the whole modelClass hierarchy to create the nested Builder instance
        while (reference != null) {
            // collect parameters and set them in the instance object
            for (ModelField modelField : reference.getFields(getGeneratedVersion())) {
                if (modelField instanceof ModelAssociation) {
                    ModelAssociation modelAssociation = (ModelAssociation) modelField;
                    JavaFieldMetadata javaFieldMetadata =
                            (JavaFieldMetadata) modelField.getMetadata(JavaFieldMetadata.ID);
                    JavaAssociationMetadata javaAssociationMetadata = getJavaAssociationMetadata(modelAssociation);

                    if (modelAssociation.isManyMultiplicity()
                            && !javaFieldMetadata.isGetter()
                            && !javaFieldMetadata.isSetter()
                            && !javaAssociationMetadata.isAdder()) {
                        throw new ModelloException(
                                "Exception while generating Java, Model inconsistency found: impossible to generate '"
                                        + modelClass.getName()
                                        + ".Builder#build()' method, '"
                                        + modelClass.getName()
                                        + "."
                                        + modelAssociation.getName()
                                        + "' field ("
                                        + modelAssociation.getType()
                                        + ") cannot be set, no getter/setter/adder method available.");
                    }

                    createSetBuilderAssociationToInstance(ctorArgs, modelAssociation, sc);
                } else {
                    createSetBuilderFieldToInstance(ctorArgs, modelField, sc);
                }
            }

            if (reference.hasSuperClass()) {
                reference = reference.getModel().getClass(reference.getSuperClass(), getGeneratedVersion());
            } else {
                reference = null;
            }
        }

        sc.add("return instance;");
    }

    private void createBuilderField(JClass jClass, ModelField modelField) throws ModelloException {
        JField field = createField(modelField);

        jClass.addField(field);

        jClass.addMethod(createSetter(field, modelField, true));
    }

    private boolean createSetBuilderFieldToInstance(Set<String> ctorArgs, ModelField modelField, JSourceCode sc)
            throws ModelloException {
        JavaFieldMetadata javaFieldMetadata = (JavaFieldMetadata) modelField.getMetadata(JavaFieldMetadata.ID);

        // if it is not already set by the ctor and if the setter method is available
        if (!ctorArgs.contains(modelField.getName()) && javaFieldMetadata.isSetter()) {
            sc.add("instance.set" + capitalise(modelField.getName()) + "( " + modelField.getName() + " );");
            return true;
        }

        return false;
    }

    private void createBuilderAssociation(JClass jClass, ModelAssociation modelAssociation) throws ModelloException {
        JavaAssociationMetadata javaAssociationMetadata = getJavaAssociationMetadata(modelAssociation);

        if (modelAssociation.isManyMultiplicity()) {
            JType componentType = getComponentType(modelAssociation, javaAssociationMetadata);
            String defaultValue = getDefaultValue(modelAssociation, componentType);

            JType type;
            if (modelAssociation.isGenericType()) {
                type = new JCollectionType(modelAssociation.getType(), componentType);
            } else if (ModelDefault.MAP.equals(modelAssociation.getType())) {
                JMapType mapType = new JMapType(modelAssociation.getType(), defaultValue, componentType);
                defaultValue = mapType.getInstanceName();
                type = mapType;
            } else {
                type = new JClass(modelAssociation.getType());
            }

            JField jField = new JField(type, modelAssociation.getName());
            jField.getModifiers().setFinal(true);

            if (!isEmpty(modelAssociation.getComment())) {
                jField.setComment(modelAssociation.getComment());
            }

            if (!modelAssociation.getAnnotations().isEmpty()) {
                for (String annotation : modelAssociation.getAnnotations()) {
                    jField.appendAnnotation(annotation);
                }
            }

            jField.setInitString(defaultValue);

            jClass.addField(jField);

            createAdder(modelAssociation, jClass, true);
        } else {
            createBuilderField(jClass, modelAssociation);
        }
    }

    private void createSetBuilderAssociationToInstance(
            Set<String> ctorArgs, ModelAssociation modelAssociation, JSourceCode sc) throws ModelloException {
        if (modelAssociation.isManyMultiplicity()) {
            // Map/Properties don't have bidi association, they can be directly set
            if (modelAssociation.getType().equals(ModelDefault.PROPERTIES)
                    || modelAssociation.getType().equals(ModelDefault.MAP)) {
                if (createSetBuilderFieldToInstance(ctorArgs, modelAssociation, sc)) {
                    return;
                }
            }

            // check if there's no bidi association, so

            JavaAssociationMetadata javaAssociationMetadata = getJavaAssociationMetadata(modelAssociation);

            boolean bidirectionalAssociation = isBidirectionalAssociation(modelAssociation);

            if (!bidirectionalAssociation || !javaAssociationMetadata.isBidi()) {
                JavaFieldMetadata javaFieldMetadata =
                        (JavaFieldMetadata) modelAssociation.getMetadata(JavaFieldMetadata.ID);

                // just use the plain old setter
                if (createSetBuilderFieldToInstance(ctorArgs, modelAssociation, sc)) {
                    return;
                }
                // or we can try to set by using the addAll if there is a getter available
                else if (javaFieldMetadata.isGetter()) {
                    String action = isMap(modelAssociation.getType()) ? "put" : "add";

                    sc.add("instance.get" + capitalise(modelAssociation.getName()) + "()." + action + "All( "
                            + modelAssociation.getName() + " );");
                    return;
                }
            }

            // no previous precondition satisfied
            // or no one of the previous method worked
            // use the adder
            // bidi association can be handled directly by the model, not a Builder task

            String itemType;
            String targetField = modelAssociation.getName();

            if (StringUtils.isNotEmpty(javaAssociationMetadata.getInterfaceName())) {
                itemType = javaAssociationMetadata.getInterfaceName();
            } else if (modelAssociation.getToClass() != null) {
                itemType = modelAssociation.getToClass().getName();
            } else if (modelAssociation.getType().equals(ModelDefault.PROPERTIES)
                    || modelAssociation.getType().equals(ModelDefault.MAP)) {
                StringBuilder itemTypeBuilder = new StringBuilder("java.util.Map.Entry");

                itemTypeBuilder.append('<');

                if (modelAssociation.getType().equals(ModelDefault.PROPERTIES)) {
                    itemTypeBuilder.append("Object, Object");
                } else {
                    itemTypeBuilder.append("String, ").append(modelAssociation.getTo());
                }

                itemTypeBuilder.append('>');

                itemType = itemTypeBuilder.toString();

                targetField += ".entrySet()";
            } else {
                itemType = "String";
            }

            sc.add("for ( " + itemType + " item : " + targetField + " )");

            sc.add("{");
            sc.indent();

            StringBuilder adder = new StringBuilder("instance.add")
                    .append(capitalise(singular(modelAssociation.getName())))
                    .append("( ");

            if (modelAssociation.getType().equals(ModelDefault.PROPERTIES)
                    || modelAssociation.getType().equals(ModelDefault.MAP)) {
                appendEntryMethod("String", "getKey()", adder, modelAssociation);
                adder.append(", ");
                appendEntryMethod(modelAssociation.getTo(), "getValue()", adder, modelAssociation);
            } else {
                adder.append("item");
            }

            adder.append(" );");

            sc.add(adder.toString());

            sc.unindent();
            sc.add("}");
        } else {
            createSetBuilderFieldToInstance(ctorArgs, modelAssociation, sc);
        }
    }

    private void appendEntryMethod(
            String type, String method, StringBuilder target, ModelAssociation modelAssociation) {
        if (modelAssociation.getType().equals(ModelDefault.PROPERTIES)) {
            target.append('(').append(type).append(") ");
        }

        target.append("item.").append(method);
    }

    private void generateStaticCreator(ModelClass modelClass, JClass jClass, JConstructor constructor)
            throws ModelloException {
        JMethod creatorMethod = new JMethod(
                "new" + modelClass.getName() + "Instance",
                new JClass(modelClass.getName()),
                "a new <code>" + modelClass.getName() + "</code> instance.");
        creatorMethod.getModifiers().setStatic(true);
        creatorMethod.getJDocComment().setComment("Creates a new <code>" + modelClass.getName() + "</code> instance.");

        ModelClass reference = modelClass;

        boolean hasDefaults = false;

        // traverse the whole modelClass hierarchy to create the static creator method
        while (reference != null) {
            for (ModelField modelField : reference.getFields(getGeneratedVersion())) {
                // this is hacky
                JField field = createField(modelField);
                creatorMethod.addParameter(new JParameter(field.getType(), field.getName()));

                if (!StringUtils.isEmpty(modelField.getDefaultValue())) {
                    hasDefaults = true;
                }
            }

            if (reference.hasSuperClass()) {
                reference = reference.getModel().getClass(reference.getSuperClass(), getGeneratedVersion());
            } else {
                reference = null;
            }
        }

        JSourceCode sc = creatorMethod.getSourceCode();

        createInstanceAndSetProperties(modelClass, constructor, sc);

        jClass.addMethod(creatorMethod);

        // creates a shortcut with default values only if necessary
        if (!hasDefaults) {
            return;
        }

        creatorMethod = new JMethod(
                "new" + modelClass.getName() + "Instance",
                new JClass(modelClass.getName()),
                "a new <code>" + modelClass.getName() + "</code> instance.");
        creatorMethod.getModifiers().setStatic(true);
        creatorMethod.getJDocComment().setComment("Creates a new <code>" + modelClass.getName() + "</code> instance.");

        StringBuilder shortcutArgs = new StringBuilder();

        reference = modelClass;

        while (reference != null) {
            for (ModelField modelField : reference.getFields(getGeneratedVersion())) {
                if (shortcutArgs.length() > 0) {
                    shortcutArgs.append(',');
                }

                shortcutArgs.append(' ');

                if (StringUtils.isEmpty(modelField.getDefaultValue())) {
                    // this is hacky
                    JField field = createField(modelField);
                    creatorMethod.addParameter(new JParameter(field.getType(), field.getName()));

                    shortcutArgs.append(modelField.getName());
                } else {
                    shortcutArgs.append(getJavaDefaultValue(modelField));
                }

                shortcutArgs.append(' ');
            }

            if (reference.hasSuperClass()) {
                reference = reference.getModel().getClass(reference.getSuperClass(), getGeneratedVersion());
            } else {
                reference = null;
            }
        }

        sc = creatorMethod.getSourceCode();

        sc.add("return new" + modelClass.getName() + "Instance(" + shortcutArgs + ");");

        jClass.addMethod(creatorMethod);
    }
}
