/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.openjpa.enhance;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.conf.OpenJPAConfigurationImpl;
import org.apache.openjpa.lib.conf.Configuration;
import org.apache.openjpa.lib.conf.Configurations;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.meta.ClassArgParser;
import org.apache.openjpa.lib.util.ClassUtil;
import org.apache.openjpa.lib.util.CodeFormat;
import org.apache.openjpa.lib.util.Files;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.Options;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.meta.AccessCode;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.DelegatingMetaDataFactory;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.MetaDataFactory;
import org.apache.openjpa.meta.MetaDataModes;
import org.apache.openjpa.meta.MetaDataRepository;
import org.apache.openjpa.util.GeneratedClasses;
import org.apache.openjpa.util.InvalidStateException;
import org.apache.openjpa.util.UserException;
import org.apache.xbean.asm9.ClassWriter;
import org.apache.xbean.asm9.Opcodes;
import org.apache.xbean.asm9.Type;

/**
 * Generates a class appropriate for use as an application identity class.
 *
 * @author Patrick Linskey
 * @author Abe White
 */
public class ApplicationIdTool {

    public static final String TOKEN_DEFAULT = "::";

    private static final String TOKENIZER_CUSTOM = "Tokenizer";
    private static final String TOKENIZER_STD = "StringTokenizer";

    private static final Localizer _loc = Localizer.forPackage
        (ApplicationIdTool.class);

    private final Log _log;
    private final Class _type;
    private final ClassMetaData _meta;
    private boolean _abstract = false;
    private FieldMetaData[] _fields = null;
    private boolean _ignore = true;
    private File _dir = null;
    private Writer _writer = null;
    private String _code = null;
    private String _token = TOKEN_DEFAULT;
    private CodeFormat _format = null;

    /**
     * Constructs a new ApplicationIdTool capable of generating an
     * object id class for <code>type</code>.
     */
    public ApplicationIdTool(OpenJPAConfiguration conf, Class type) {
        _log = conf.getLog(OpenJPAConfiguration.LOG_ENHANCE);
        _type = type;

        MetaDataRepository repos = conf.newMetaDataRepositoryInstance();
        repos.setValidate(MetaDataRepository.VALIDATE_NONE);
        repos.setSourceMode(MetaDataModes.MODE_MAPPING, false);
        loadObjectIds(repos, true);
        _meta = repos.getMetaData(type, null, false);
        if (_meta != null) {
            _abstract = Modifier.isAbstract(_meta.getDescribedType().
                getModifiers());
            _fields = getDeclaredPrimaryKeyFields(_meta);
        }
    }

    /**
     * Constructs a new tool instance capable of generating an
     * object id class for <code>meta</code>.
     */
    public ApplicationIdTool(OpenJPAConfiguration conf, Class type, ClassMetaData meta) {
        _log = conf.getLog(OpenJPAConfiguration.LOG_ENHANCE);

        _type = type;
        _meta = meta;
        if (_meta != null) {
            _abstract = Modifier.isAbstract(_meta.getDescribedType().
                getModifiers());
            _fields = getDeclaredPrimaryKeyFields(_meta);
        }
    }

    /**
     * Return metadata for primary key fields declared in the given class.
     */
    private static FieldMetaData[] getDeclaredPrimaryKeyFields
        (ClassMetaData meta) {
        if (meta.getPCSuperclass() == null)
            return meta.getPrimaryKeyFields();

        // remove the primary key fields that are not declared
        // in the current class
        FieldMetaData[] fields = meta.getPrimaryKeyFields();
        List decs = new ArrayList(fields.length);
        for (FieldMetaData field : fields)
            if (field.getDeclaringType() == meta.getDescribedType())
                decs.add(field);
        return (FieldMetaData[]) decs.toArray(new FieldMetaData[decs.size()]);
    }

    /**
     * Return false if this tool is configured to throw an exception on
     * an attempt to generate an id class for a type that does not use
     * application identity.
     */
    public boolean getIgnoreErrors() {
        return _ignore;
    }

    /**
     * Set to false if this tool should throw an exception on
     * an attempt to generate an id class for a type that does not use
     * application identity.
     */
    public void setIgnoreErrors(boolean ignore) {
        _ignore = ignore;
    }

    /**
     * The code formatter for the generated Java code.
     */
    public CodeFormat getCodeFormat() {
        return _format;
    }

    /**
     * Set the code formatter for the generated Java code.
     */
    public void setCodeFormat(CodeFormat format) {
        _format = format;
    }

    /**
     * The directory to write source to. Defaults to the directory
     * of the Java file for the set type. If the given directory does not
     * match the package of the object id, the package structure will be
     * created below the directory.
     */
    public File getDirectory() {
        return _dir;
    }

    /**
     * The directory to write source to. Defaults to the directory
     * of the Java file for the set type. If the given directory does not
     * match the package of the object id, the package structure will be
     * created below the directory.
     */
    public void setDirectory(File dir) {
        _dir = dir;
    }

    /**
     * The token to use to separate stringified primary key field values.
     */
    public String getToken() {
        return _token;
    }

    /**
     * The token to use to separate stringified primary key field values.
     */
    public void setToken(String token) {
        _token = token;
    }

    /**
     * The writer to write source to, or null to write to default file.
     */
    public Writer getWriter() {
        return _writer;
    }

    /**
     * The writer to write source to, or null to write to default file.
     */
    public void setWriter(Writer writer) {
        _writer = writer;
    }

    /**
     * Return the type we are generating an application id for.
     */
    public Class getType() {
        return _type;
    }

    /**
     * Return metadata for the type we are generating an application id for.
     */
    public ClassMetaData getMetaData() {
        return _meta;
    }

    /**
     * Return the code generated for the application id, or null
     * if invalid class or the {@link #run} method has not been called.
     */
    public String getCode() {
        return _code;
    }

    /**
     * Returns true if the application identity class should be an inner class.
     */
    public boolean isInnerClass() {
        Class oidClass = _meta.getObjectIdType();
        return oidClass.getName().indexOf('$') != -1;
    }

    /**
     * Returns the short class name for the object id class.
     */
    private String getClassName() {
        if (_meta.isOpenJPAIdentity())
            return null;

        // convert from SomeClass$ID to ID
        String className = ClassUtil.getClassName(_meta.getObjectIdType());
        if (isInnerClass())
            className = className.substring(className.lastIndexOf('$') + 1);
        return className;
    }

    /**
     * Generates the sourcecode for the application id class; returns
     * false if the class is invalid.
     */
    public boolean run() {
        if (_log.isInfoEnabled())
            _log.info(_loc.get("appid-start", _type));

        // ensure that this type is a candidate for application identity
        if (_meta == null
            || _meta.getIdentityType() != ClassMetaData.ID_APPLICATION
            || _meta.isOpenJPAIdentity()) {
            if (!_ignore)
                throw new UserException(_loc.get("appid-invalid", _type));

            // else just warn
            if (_log.isWarnEnabled())
                _log.warn(_loc.get("appid-warn", _type));
            return false;
        }

        Class oidClass = _meta.getObjectIdType();
        Class superOidClass = null;

        // allow diff oid class in subclass (horizontal)
        if (_meta.getPCSuperclass() != null) {
            superOidClass = _meta.getPCSuperclassMetaData().getObjectIdType();
            if (oidClass == null || oidClass.equals(superOidClass)) {
                // just warn
                if (_log.isWarnEnabled())
                    _log.warn(_loc.get("appid-warn", _type));
                return false;
            }
        }

        // ensure that an id class is declared
        if (oidClass == null)
            throw new UserException(_loc.get("no-id-class", _type)).
                setFatal(true);

        // ensure there is at least one pk field if we are
        // non-absract, and see if we have any byte[]
        boolean bytes = false;
        for (int i = 0; !bytes && i < _fields.length; i++)
            bytes = _fields[i].getDeclaredType() == byte[].class;

        // collect info on id type
        String className = getClassName();
        String packageName = ClassUtil.getPackageName(oidClass);
        String packageDec = "";
        if (packageName.length() > 0)
            packageDec = "package " + packageName + ";";

        String imports = getImports();
        String fieldDecs = getFieldDeclarations();
        String constructor = getConstructor(superOidClass != null);
        String properties = getProperties();
        String fromStringCode = getFromStringCode(superOidClass != null);
        String toStringCode = getToStringCode(superOidClass != null);
        String equalsCode = getEqualsCode(superOidClass != null);
        String hashCodeCode = getHashCodeCode(superOidClass != null);

        // build the java code
        CodeFormat code = newCodeFormat();
        if (!isInnerClass() && packageDec.length() > 0)
            code.append(packageDec).afterSection();

        if (!isInnerClass() && imports.length() > 0)
            code.append(imports).afterSection();

        code.append("/**").endl().
            append(" * ").
            append(_loc.get("appid-comment-for", _type.getName())).
            endl().
            append(" *").endl().
            append(" * ").append(_loc.get("appid-comment-gen")).endl().
            append(" * ").append(getClass().getName()).endl().
            append(" */").endl();
        code.append("public ");
        if (isInnerClass())
            code.append("static ");
        code.append("class ").append(className);
        if (code.getBraceOnSameLine())
            code.append(" ");
        else
            code.endl().tab();

        if (superOidClass != null) {
            code.append("extends " + ClassUtil.getClassName(superOidClass));
            if (code.getBraceOnSameLine())
                code.append(" ");
            else
                code.endl().tab();
        }
        code.append("implements Serializable").openBrace(1).endl();

        // if we use a byte array we need a static array for encoding to string
        if (bytes) {
            code.tab().append("private static final char[] HEX = ").
                append("new char[] {").endl();
            code.tab(2).append("'0', '1', '2', '3', '4', '5', '6', '7',").
                endl();
            code.tab(2).append("'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'").
                endl();
            code.tab().append("};").endl(2);
        }

        // static block to register class
        code.tab().append("static").openBrace(2).endl();
        code.tab(2).append("// register persistent class in JVM").endl();
        code.tab(2).append("try { Class.forName").openParen(true).
                append("\"").append(_type.getName()).append("\"").
                closeParen().append(";").append(" }").endl();
        code.tab(2).append("catch").openParen(true).
                append("Exception e").closeParen().append(" {}").endl();

        code.closeBrace(2);

        // field declarations
        if (fieldDecs.length() > 0)
            code.endl(2).append(fieldDecs);

        // default constructor
        code.afterSection().tab().append("public ").append(className).
            parens().openBrace(2).endl();
        code.closeBrace(2);

        // string constructor
        code.afterSection().append(constructor);

        // properties
        if (properties.length() > 0)
            code.afterSection().append(properties);

        // toString, equals, hashCode methods
        if (toStringCode.length() > 0)
            code.afterSection().append(toStringCode);
        if (hashCodeCode.length() > 0)
            code.afterSection().append(hashCodeCode);
        if (equalsCode.length() > 0)
            code.afterSection().append(equalsCode);
        if (fromStringCode.length() > 0)
            code.afterSection().append(fromStringCode);

        // if we have any byte array fields, we have to add the extra
        // methods for handling byte arrays
        if (bytes) {
            code.afterSection().append(getToBytesByteArrayCode());
            code.afterSection().append(getToStringByteArrayCode());
            code.afterSection().append(getEqualsByteArrayCode());
            code.afterSection().append(getHashCodeByteArrayCode());
        }

        // base classes might need to define a custom tokenizer
        if (superOidClass == null && getTokenizer(false) == TOKENIZER_CUSTOM)
            code.afterSection().append(getCustomTokenizerClass());

        code.endl();
        code.closeBrace(1);

        _code = code.toString();

        // if this is an inner class, then indent the entire
        // code unit one tab level
        if (isInnerClass()) {
            // indent the entire code block one level to make it
            // a propertly indented innder class
            _code = code.getTab() +
                    StringUtil.replace(_code,
                                       J2DoPrivHelper.getLineSeparator(),
                                       J2DoPrivHelper.getLineSeparator() + code.getTab());
        }

        return true;
    }

    /**
     * Writes the generated code to the proper file.
     */
    public void record()
        throws IOException {
        if (_code == null)
            return;

        Writer writer = _writer;
        if (writer == null) {
            File file = getFile();
            Files.backup(file, false);
            writer = new FileWriter(file);
        }

        PrintWriter printer = new PrintWriter(writer);
        printer.print(_code);
        printer.flush();

        if (_writer == null)
            writer.close();
    }

    /**
     * Return the necessary imports for the class.
     */
    private String getImports() {
        Set pkgs = getImportPackages();

        CodeFormat imports = newCodeFormat();
        String base = ClassUtil.getPackageName(_meta.getObjectIdType());
        String pkg;
        for (Object o : pkgs) {
            pkg = (String) o;
            if (pkg.length() > 0 && !"java.lang".equals(pkg)
                    && !base.equals(pkg)) {
                if (imports.length() > 0)
                    imports.endl();
                imports.append("import ").append(pkg).append(".*;");
            }
        }
        return imports.toString();
    }

    /**
     * Returns the collection of packages that need to be imported.
     */
    public Set getImportPackages() {
        Set pkgs = new TreeSet();
        pkgs.add(ClassUtil.getPackageName(_type));

        Class superOidClass = null;
        if (_meta != null && _meta.getPCSuperclassMetaData() != null)
            superOidClass = _meta.getPCSuperclassMetaData().getObjectIdType();
        if (superOidClass != null)
            pkgs.add(ClassUtil.getPackageName(superOidClass));

        pkgs.add("java.io");
        pkgs.add("java.util");
        Class type;
        for (FieldMetaData field : _fields) {
            type = field.getObjectIdFieldType();
            if (type != byte[].class && type != char[].class
                    && !type.getName().startsWith("java.sql.")) {
                pkgs.add(ClassUtil.getPackageName(type));
            }
        }
        return pkgs;
    }

    /**
     * Return the code to declare all primary key fields.
     */
    private String getFieldDeclarations() {
        CodeFormat code = newCodeFormat();
        for (int i = 0; i < _fields.length; i++) {
            if (i > 0)
                code.endl();
            code.tab().append("public ").append(getTypeName(_fields[i])).
                append(" ").append(_fields[i].getName()).append(";");
        }
        return code.toString();
    }

    /**
     * Return the type name to declare the given field as.
     */
    private String getTypeName(FieldMetaData fmd) {
        Class type = fmd.getObjectIdFieldType();
        if (type == byte[].class)
            return "byte[]";
        if (type == char[].class)
            return "char[]";
        if (type.getName().startsWith("java.sql."))
            return type.getName();
        return ClassUtil.getClassName(type);
    }

    /**
     * Return the getters and setters for all primary key fields.
     */
    private String getProperties() {
        if (AccessCode.isExplicit(_meta.getAccessType())
         && AccessCode.isField(_meta.getAccessType()))
            return "";

        CodeFormat code = newCodeFormat();
        String propName;
        String typeName;
        for (int i = 0; i < _fields.length; i++) {
            if (i > 0)
                code.afterSection();
            typeName = getTypeName(_fields[i]);
            propName = StringUtil.capitalize(_fields[i].getName());

            code.tab().append("public ").append(typeName).append(" ");
            if (_fields[i].getDeclaredTypeCode() == JavaTypes.BOOLEAN
                || _fields[i].getDeclaredTypeCode() == JavaTypes.BOOLEAN_OBJ)
                code.append("is");
            else
                code.append("get");
            code.append(propName).parens().openBrace(2).endl();
            code.tab(2).append("return ").append(_fields[i].getName()).
                append(";").endl();
            code.closeBrace(2);
            code.afterSection();

            code.tab().append("public void set").append(propName);
            code.openParen(true).append(typeName).append(" ").
                append(_fields[i].getName()).closeParen();
            code.openBrace(2).endl();
            code.tab(2).append("this.").append(_fields[i].getName()).
                append(" = ").append(_fields[i].getName()).append(";").
                endl();
            code.closeBrace(2);
        }
        return code.toString();
    }

    /**
     * Return the string constructor code.
     */
    private String getConstructor(boolean hasSuperclass) {
        CodeFormat code = newCodeFormat();
        code.tab().append("public ");
        code.append(getClassName());
        code.openParen(true).append("String str").closeParen();
        code.openBrace(2).endl();

        if (_fields.length != 0 || (hasSuperclass
            && _meta.getPrimaryKeyFields().length > 0)) {
            code.tab(2).append("fromString").openParen(true).
                append("str").closeParen().append(";").endl();
        }

        code.closeBrace(2);
        return code.toString();
    }

    /**
     * Create the fromString method that parses the result of our toString
     * method. If we have superclasses with id fields, this will call
     * super.fromString() so that the parent class can parse its own fields.
     */
    private String getFromStringCode(boolean hasSuperclass) {
        // if we are below a concrete class then we cannot declare any
        // more primary key fields; thus, just use the parent invocation
        if (hasConcreteSuperclass())
            return "";
        if (_fields.length == 0)
            return "";
        hasSuperclass = hasSuperclass && getDeclaredPrimaryKeyFields
            (_meta.getPCSuperclassMetaData()).length > 0;

        String toke = getTokenizer(hasSuperclass);
        CodeFormat code = newCodeFormat();
        if (_abstract || hasSuperclass)
            code.tab().append("protected ").append(toke).
                append(" fromString");
        else
            code.tab().append("private void fromString");
        code.openParen(true).append("String str").closeParen();
        code.openBrace(2).endl();

        // if we have any Object-type fields, die immediately
        for (FieldMetaData fieldMetaData : _fields) {
            if (fieldMetaData.getObjectIdFieldType() != Object.class)
                continue;
            code.tab(2).append("throw new UnsupportedOperationException").
                    parens().append(";").endl();
            code.closeBrace(2);
            return code.toString();
        }

        if (toke != null) {
            code.tab(2).append(toke).append(" toke = ");
            if (hasSuperclass) {
                // call super.fromString(str) to get the tokenizer that was
                // used to parse the superclass
                code.append("super.fromString").openParen(true).
                    append("str").closeParen();
            } else {
                // otherwise construct a new tokenizer with the string
                code.append("new ").append(toke).openParen(true).
                    append("str");
                if (toke == TOKENIZER_STD)
                    code.append(", \"").append(_token).append("\"");
                code.closeParen();
            }
            code.append(";").endl();
        }

        for (FieldMetaData field : _fields) {
            if (toke != null) {
                code.tab(2).append("str = toke.nextToken").parens().
                        append(";").endl();
            }
            code.tab(2).append(getConversionCode(field, "str")).endl();
        }
        if (_abstract || hasSuperclass)
            code.tab(2).append("return toke;").endl();
        code.closeBrace(2);
        return code.toString();
    }

    /**
     * Returns the type of tokenizer to use, or null if none.
     */
    private String getTokenizer(boolean hasSuperclass) {
        if (!_abstract && !hasSuperclass && _fields.length == 1)
            return null;
        if (_token.length() == 1)
            return TOKENIZER_STD;
        return TOKENIZER_CUSTOM;
    }

    /**
     * Get parsing code for the given field.
     */
    private String getConversionCode(FieldMetaData field, String var) {
        CodeFormat parse = newCodeFormat();
        if (field.getName().equals(var))
            parse.append("this.");
        parse.append(field.getName()).append(" = ");

        Class type = field.getObjectIdFieldType();
        if (type == Date.class) {
            parse.append("new Date").openParen(true).
                append("Long.parseLong").openParen(true).
                append(var).closeParen().closeParen();
        } else if (type == java.sql.Date.class
            || type == java.sql.Timestamp.class
            || type == java.sql.Time.class) {
            parse.append(type.getName()).append(".valueOf").openParen(true).
                append(var).closeParen();
        } else if (type == String.class)
            parse.append(var);
        else if (type == Character.class) {
            parse.append("new Character").openParen(true).append(var).
                append(".charAt").openParen(true).append(0).
                closeParen().closeParen();
        } else if (type == byte[].class)
            parse.append("toBytes").openParen(true).append(var).closeParen();
        else if (type == char[].class)
            parse.append(var).append(".toCharArray").parens();
        else if (!type.isPrimitive()) {
            parse.append("new ").append(ClassUtil.getClassName(type)).
                openParen(true).append(var).closeParen();
        } else // primitive
        {
            switch (type.getName().charAt(0)) {
                case 'b':
                    if (type == boolean.class)
                        parse.append("\"true\".equals").openParen(true).
                            append(var).closeParen();
                    else
                        parse.append("Byte.parseByte").openParen(true).
                            append(var).closeParen();
                    break;
                case 'c':
                    parse.append(var).append(".charAt").openParen(true).
                        append(0).closeParen();
                    break;
                case 'd':
                    parse.append("Double.parseDouble").openParen(true).
                        append(var).closeParen();
                    break;
                case 'f':
                    parse.append("Float.parseFloat").openParen(true).
                        append(var).closeParen();
                    break;
                case 'i':
                    parse.append("Integer.parseInt").openParen(true).
                        append(var).closeParen();
                    break;
                case 'l':
                    parse.append("Long.parseLong").openParen(true).
                        append(var).closeParen();
                    break;
                case 's':
                    parse.append("Short.parseShort").openParen(true).
                        append(var).closeParen();
                    break;
            }
        }

        if (!type.isPrimitive() && type != byte[].class) {
            CodeFormat isNull = newCodeFormat();
            isNull.append("if").openParen(true).append("\"null\".equals").
                openParen(true).append(var).closeParen().closeParen().
                endl().tab(3);
            if (field.getName().equals(var))
                isNull.append("this.");
            isNull.append(field.getName()).append(" = null;").endl();
            isNull.tab(2).append("else").endl();
            isNull.tab(3).append(parse);
            parse = isNull;
        }

        return parse.append(";").toString();
    }

    /**
     * Return an equality method that compares all pk variables.
     * Must deal correctly with both primitives and objects.
     */
    private String getEqualsCode(boolean hasSuperclass) {
        // if we are below a concrete class then we cannot declare any
        // more primary key fields; thus, just use the parent invocation
        if (hasConcreteSuperclass() || (hasSuperclass && _fields.length == 0))
            return "";

        CodeFormat code = newCodeFormat();
        code.tab().append("public boolean equals").openParen(true).
            append("Object obj").closeParen().openBrace(2).endl();

        code.tab(2).append("if").openParen(true).
            append("this == obj").closeParen().endl();
        code.tab(3).append("return true;").endl();

        // call super.equals() if we have a superclass
        String className = getClassName();
        if (hasSuperclass) {
            code.tab(2).append("if").openParen(true).
                append("!super.equals").openParen(true).
                append("obj").closeParen().closeParen().endl();
            code.tab(3).append("return false;").endl();
        } else {
            code.tab(2).append("if").openParen(true).
                append("obj == null || obj.getClass").parens().
                append(" != ").append("getClass").parens().
                closeParen().endl();
            code.tab(3).append("return false;").endl();
        }

        String name;
        Class type;
        for (int i = 0; i < _fields.length; i++) {
            if (i == 0) {
                code.endl().tab(2).append(className).append(" other = ").
                    openParen(false).append(className).closeParen().
                    append(" obj;").endl();
            }

            // if this is not the first field, add an &&
            if (i == 0)
                code.tab(2).append("return ");
            else
                code.endl().tab(3).append("&& ");

            name = _fields[i].getName();
            type = _fields[i].getObjectIdFieldType();
            if (type.isPrimitive()) {
                code.openParen(false).append(name).append(" == ").
                    append("other.").append(name).closeParen();
            } else if (type == byte[].class) {
                code.openParen(false).append("equals").openParen(true).
                    append(name).append(", ").append("other.").
                    append(name).closeParen().closeParen();
            } else if (type == char[].class) {
                // ((name == null && other.name == null)
                //    || (name != null && String.valueOf (name).
                //    equals (String.valueOf (other.name))))
                code.append("(").openParen(false).append(name).
                    append(" == null && other.").append(name).
                    append(" == null").closeParen().endl();
                code.tab(3).append("|| ");
                code.openParen(false).append(name).append(" != null ").
                    append("&& String.valueOf").openParen(true).append(name).
                    closeParen().append(".").endl();
                code.tab(3).append("equals").openParen(true).
                    append("String.valueOf").openParen(true).
                    append("other.").append(name).closeParen().closeParen().
                    closeParen().append(")");
            } else {
                // ((name == null && other.name == null)
                //    || (name != null && name.equals (other.name)))
                code.append("(").openParen(false).append(name).
                    append(" == null && other.").append(name).
                    append(" == null").closeParen().endl();
                code.tab(3).append("|| ");
                code.openParen(false).append(name).append(" != null ").
                    append("&& ").append(name).append(".equals").
                    openParen(true).append("other.").append(name).
                    closeParen().closeParen().append(")");
            }
        }

        // no _fields: just return true after checking instanceof
        if (_fields.length == 0)
            code.tab(2).append("return true;").endl();
        else
            code.append(";").endl();

        code.closeBrace(2);
        return code.toString();
    }

    /**
     * Return a hashCode method that takes into account all
     * primary key values. Must deal correctly with both primitives and objects.
     */
    private String getHashCodeCode(boolean hasSuperclass) {
        // if we are below a concrete class then we cannot declare any
        // more primary key fields; thus, just use the parent invocation
        if (hasConcreteSuperclass() || (hasSuperclass && _fields.length == 0))
            return "";

        CodeFormat code = newCodeFormat();
        code.tab().append("public int hashCode").parens().
            openBrace(2).endl();

        if (_fields.length == 0)
            code.tab(2).append("return 17;").endl();
        else if (_fields.length == 1 && !hasSuperclass) {
            code.tab(2).append("return ");
            appendHashCodeCode(_fields[0], code);
            code.append(";").endl();
        } else {
            code.tab(2).append("int rs = ");
            if (hasSuperclass) {
