/*
 * 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.persistence;

import static org.apache.openjpa.meta.MetaDataModes.MODE_MAPPING;
import static org.apache.openjpa.meta.MetaDataModes.MODE_META;
import static org.apache.openjpa.meta.MetaDataModes.MODE_NONE;
import static org.apache.openjpa.meta.MetaDataModes.MODE_QUERY;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.kernel.QueryLanguages;
import org.apache.openjpa.lib.conf.Configurations;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.meta.CFMetaDataSerializer;
import org.apache.openjpa.lib.meta.SourceTracker;
import org.apache.openjpa.lib.util.ClassUtil;
import org.apache.openjpa.lib.util.JavaVersions;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.AccessCode;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.MetaDataInheritanceComparator;
import org.apache.openjpa.meta.MetaDataRepository;
import org.apache.openjpa.meta.Order;
import org.apache.openjpa.meta.QueryMetaData;
import org.apache.openjpa.meta.SequenceMetaData;
import org.apache.openjpa.meta.ValueMetaData;
import org.apache.openjpa.util.InternalException;
import org.xml.sax.SAXException;

/**
 * Serializes persistence metadata back to XML.
 * This class processes all object level tags that are store-agnostic.
 * However, it provides hooks for the subclasses to include store-specific
 * tags to be serialized both at &lt;entity-mappings&gt; and
 * &lt;entity&gt; level.
 *
 * @since 0.4.0
 * @author Steve Kim
 */
public class XMLPersistenceMetaDataSerializer
    extends CFMetaDataSerializer
    implements PersistenceMetaDataFactory.Serializer {

    // NOTE: order is important! these constants must be maintained in
    // serialization order. constants are spaced so that subclasses can
    // slip tags in-between
    protected static final int TYPE_SEQ = 10;
    protected static final int TYPE_QUERY = 20;
    protected static final int TYPE_META = 30;
    protected static final int TYPE_CLASS_SEQS = 40;
    protected static final int TYPE_CLASS_QUERIES = 50;

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

    private final OpenJPAConfiguration _conf;
    private Map<String, ClassMetaData> _metas = null;
    private Map<String, List<QueryMetaData>> _queries = null;
    private Map<String, List<SequenceMetaData>> _seqs = null;
    private int _mode = MODE_NONE;
    private boolean _annos = true;
    private SerializationComparator _comp = null;

    /**
     * Constructor. Supply configuration.
     */
    public XMLPersistenceMetaDataSerializer(OpenJPAConfiguration conf) {
        _conf = conf;
        setLog(conf.getLog(OpenJPAConfiguration.LOG_METADATA));
        setMode(MODE_META | MODE_MAPPING | MODE_QUERY);
    }

    /**
     * Configuration.
     */
    public OpenJPAConfiguration getConfiguration() {
        return _conf;
    }

    /**
     * Whether to serialize content originally specified in annotations.
     * Defaults to true.
     */
    public boolean getSerializeAnnotations() {
        return _annos;
    }

    /**
     * Whether to serialize content originally specified in annotations.
     * Defaults to true.
     */
    public void setSerializeAnnotations(boolean annos) {
        _annos = annos;
    }

    /**
     * The serialization mode according to the expected document type. The
     * mode constants act as bit flags, and therefore can be combined.
     */
    public int getMode() {
        return _mode;
    }

    /**
     * The serialization mode according to the expected document type. The
     * mode constants act as bit flags, and therefore can be combined.
     */
    @Override
    public void setMode(int mode) {
        _mode = mode;
    }

    /**
     * The serialization mode according to the expected document type.
     */
    public void setMode(int mode, boolean on) {
        if (mode == MODE_NONE)
            setMode(MODE_NONE);
        else if (on)
            setMode(_mode | mode);
        else
            setMode(_mode & ~mode);
    }

    /**
     * Override to not overwrite annotations.
     */
    @Override
    protected File getSourceFile(Object obj) {
        File file = super.getSourceFile(obj);
        if (file == null || file.getName().endsWith(".java")
            || file.getName().endsWith(".class"))
            return null;
        return file;
    }

    /**
     * Convenience method for interpreting {@link #getMode}.
     */
    protected boolean isMetaDataMode() {
        return (_mode & MODE_META) != 0;
    }

    /**
     * Convenience method for interpreting {@link #getMode}.
     */
    protected boolean isQueryMode() {
        return (_mode & MODE_QUERY) != 0;
    }

    /**
     * Convenience method for interpreting {@link #getMode}.
     */
    protected boolean isMappingMode() {
        return (_mode & MODE_MAPPING) != 0;
    }

    /**
     * Convenience method for interpreting {@link #getMode}. Takes into
     * account whether mapping information is loaded for the given instance.
     */
    protected boolean isMappingMode(ClassMetaData meta) {
        return isMappingMode() && (meta.getSourceMode() & MODE_MAPPING) != 0
            && (meta.getEmbeddingMetaData() != null
            || !meta.isEmbeddedOnly())
            && (meta.getEmbeddingMetaData() == null
            || isMappingMode(meta.getEmbeddingMetaData()));
    }

    /**
     * Convenience method for interpreting {@link #getMode}. Takes into
     * account whether mapping information is loaded for the given instance.
     */
    protected boolean isMappingMode(ValueMetaData vmd) {
        return isMappingMode(vmd.getFieldMetaData().getDefiningMetaData());
    }

    /**
     * Add a class meta data to the set to be serialized.
     */
    @Override
    public void addMetaData(ClassMetaData meta) {
        if (meta == null)
            return;

        if (_metas == null)
            _metas = new HashMap<>();
        _metas.put(meta.getDescribedType().getName(), meta);
    }

    /**
     * Add a sequence meta data to the set to be serialized.
     */
    @Override
    public void addSequenceMetaData(SequenceMetaData meta) {
        if (meta == null)
            return;

        List<SequenceMetaData> seqs = null;
        String defName = null;
        if (meta.getSourceScope() instanceof Class)
            defName = ((Class) meta.getSourceScope()).getName();
        if (_seqs == null)
            _seqs = new HashMap<>();
        else
            seqs = _seqs.get(defName);

        if (seqs == null) {
            seqs = new ArrayList<>(3); // don't expect many seqs / class
            seqs.add(meta);
            _seqs.put(defName, seqs);
        } else if (!seqs.contains(meta))
            seqs.add(meta);
    }

    /**
     * Add a query meta data to the set to be serialized.
     */
    @Override
    public void addQueryMetaData(QueryMetaData meta) {
        if (meta == null)
            return;

        List<QueryMetaData> queries = null;
        String defName = null;
        if (meta.getSourceScope() instanceof Class)
            defName = ((Class) meta.getSourceScope()).getName();
        if (_queries == null)
            _queries = new HashMap<>();
        else
            queries = _queries.get(defName);

        if (queries == null) {
            queries = new ArrayList(3); // don't expect many queries / class
            queries.add(meta);
            _queries.put(defName, queries);
        } else if (!queries.contains(meta))
            queries.add(meta);
    }

    /**
     * Add all components in the given repository to the set to be serialized.
     */
    @Override
    public void addAll(MetaDataRepository repos) {
        if (repos == null)
            return;

        for (ClassMetaData meta : repos.getMetaDatas())
            addMetaData(meta);
        for (SequenceMetaData seq : repos.getSequenceMetaDatas())
            addSequenceMetaData(seq);
        for (QueryMetaData query : repos.getQueryMetaDatas())
            addQueryMetaData(query);
    }

    /**
     * Remove a metadata from the set to be serialized.
     *
     * @return true if removed, false if not in set
     */
    @Override
    public boolean removeMetaData(ClassMetaData meta) {
        return _metas != null && meta != null
            && _metas.remove(meta.getDescribedType().getName()) != null;
    }

    /**
     * Remove a sequence metadata from the set to be serialized.
     *
     * @return true if removed, false if not in set
     */
    public boolean removeSequenceMetaData(SequenceMetaData meta) {
        if (_seqs == null || meta == null)
            return false;
        String defName = null;
        if (meta.getSourceScope() instanceof Class)
            defName = ((Class) meta.getSourceScope()).getName();
        List<SequenceMetaData> seqs = _seqs.get(defName);
        if (seqs == null)
            return false;
        if (!seqs.remove(meta))
            return false;
        if (seqs.isEmpty())
            _seqs.remove(defName);
        return true;
    }

    /**
     * Remove a query metadata from the set to be serialized.
     *
     * @return true if removed, false if not in set
     */
    public boolean removeQueryMetaData(QueryMetaData meta) {
        if (_queries == null || meta == null)
            return false;
        String defName = null;
        if (meta.getSourceScope() instanceof Class)
            defName = ((Class) meta.getSourceScope()).getName();
        List<QueryMetaData> queries = _queries.get(defName);
        if (queries == null)
            return false;
        if (!queries.remove(meta))
            return false;
        if (queries.isEmpty())
            _queries.remove(defName);
        return true;
    }

    /**
     * Remove all the components in the given repository from the set to be
     * serialized.
     *
     * @return true if any components removed, false if none in set
     */
    public boolean removeAll(MetaDataRepository repos) {
        if (repos == null)
            return false;

        boolean removed = false;
        ClassMetaData[] metas = repos.getMetaDatas();
        for (ClassMetaData meta : metas) {
            removed |= removeMetaData(meta);
        }
        SequenceMetaData[] seqs = repos.getSequenceMetaDatas();
        for (SequenceMetaData seq : seqs) {
            removed |= removeSequenceMetaData(seq);
        }
        QueryMetaData[] queries = repos.getQueryMetaDatas();
        for (QueryMetaData query : queries) {
            removed |= removeQueryMetaData(query);
        }
        return removed;
    }

    /**
     * Clear the set of metadatas to be serialized.
     */
    public void clear() {
        if (_metas != null)
            _metas.clear();
        if (_seqs != null)
            _seqs.clear();
        if (_queries != null)
            _queries.clear();
    }

    @Override
    protected Collection getObjects() {
        List all = new ArrayList();
        if (isQueryMode())
            addQueryMetaDatas(all);
        if (isMappingMode())
            addSequenceMetaDatas(all);
        if ((isMetaDataMode() || isMappingMode()) && _metas != null)
            all.addAll(_metas.values());
        if (isMappingMode())
            addSystemMappingElements(all);
        serializationSort(all);
        return all;
    }

    /**
     * Add system-level mapping elements to be serialized. Does nothing
     * by default.
     */
    protected void addSystemMappingElements(Collection toSerialize) {
    }

    /**
     * Sort the given collection of objects to be serialized.
     */
    private void serializationSort(List objs) {
        if (objs == null || objs.isEmpty())
            return;
        if (_comp == null)
            _comp = newSerializationComparator();
        Collections.sort(objs, _comp);
    }

    /**
     * Create a new comparator for ordering objects that are to be serialized.
     */
    protected SerializationComparator newSerializationComparator() {
        return _comp;
    }

    /**
     * Add sequence metadata to the given metadatas collection.
     */
    private void addSequenceMetaDatas(Collection all) {
        if (_seqs == null)
            return;

        for (Map.Entry entry : _seqs.entrySet()) {
            if (entry.getKey() == null)
                all.addAll((List) entry.getValue());
            else if (_metas == null || !_metas.containsKey(entry.getKey()))
                all.add(new ClassSeqs((List<SequenceMetaData>)
                    entry.getValue()));
        }
    }

    /**
     * Add query metadata to the given metadatas collection.
     */
    private void addQueryMetaDatas(Collection all) {
        if (_queries == null)
            return;

        for (Map.Entry entry : _queries.entrySet()) {
            if (entry.getKey() == null)
                all.addAll((List) entry.getValue());
            else if (_mode == MODE_QUERY || _metas == null
                || !_metas.containsKey(entry.getKey()))
                all.add(new ClassQueries((List<QueryMetaData>)
                    entry.getValue()));
        }
    }

    @Override
    protected void serialize(Collection objects)
        throws SAXException {
        // copy collection to avoid mutation
        Object meta;
        boolean unique = true;
        boolean fieldAccess = false;
        boolean propertyAccess = false;
        for (Object object : objects) {
            meta = object;
            switch (type(meta)) {
                case TYPE_META:
                    ClassMetaData cls = (ClassMetaData) meta;
                    if (AccessCode.isField(cls.getAccessType()))
                        fieldAccess = true;
                    else
                        propertyAccess = true;
                    // no break
                default:
                    if (unique && getPackage() == null)
                        setPackage(getPackage(meta));
                    else if (unique) {
                        unique = getPackage().equals(getPackage(meta));
                        if (!unique)
                            setPackage(null);
                    }
            }
        }

        serializeNamespaceAttributes();
        startElement("entity-mappings");
        if (getPackage() != null) {
            startElement("package");
            addText(getPackage());
            endElement("package");
        }
        if (fieldAccess != propertyAccess) // i.e. only one
        {
            int def = getConfiguration().getMetaDataRepositoryInstance().
                getMetaDataFactory().getDefaults().getDefaultAccessType();
            String access = null;
            if (fieldAccess && AccessCode.isProperty(def))
                access = "FIELD";
            else if (propertyAccess && AccessCode.isField(def))
                access = "PROPERTY";
            if (access != null) {
                startElement("access");
                addText(access);
                endElement("access");
            }
        }
        for (Object obj : objects) {
            int type = type(obj);
            switch (type) {
                case TYPE_META:
                    serializeClass((ClassMetaData) obj, fieldAccess
                        && propertyAccess);
                    break;
                case TYPE_SEQ:
                    if (isMappingMode())
                        serializeSequence((SequenceMetaData) obj);
                    break;
                case TYPE_QUERY:
                    serializeQuery((QueryMetaData) obj);
                    break;
                case TYPE_CLASS_QUERIES:
                    for (QueryMetaData query : ((ClassQueries) obj).getQueries())
                        serializeQuery(query);
                    break;
                case TYPE_CLASS_SEQS:
                    if (isMappingMode())
                        for (SequenceMetaData seq : ((ClassSeqs) obj).getSequences())
                            serializeSequence(seq);
                    break;
                default:
                    if (isMappingMode())
                        serializeSystemMappingElement(obj);
                    break;
            }
        }
        endElement("entity-mappings");
    }

    @Override
    protected String getPackage(Object obj) {
        int type = type(obj);
        switch (type) {
            case TYPE_META:
                return ClassUtil.getPackageName(((ClassMetaData) obj).
                    getDescribedType());
            case TYPE_QUERY:
            case TYPE_SEQ:
            case TYPE_CLASS_QUERIES:
            case TYPE_CLASS_SEQS:
                SourceTracker st = (SourceTracker) obj;
                if (st.getSourceScope() instanceof Class)
                    return ClassUtil.getPackageName((Class) st.getSourceScope());
                return null;
            default:
                return null;
        }
    }

    /**
     * Return the type constant for the given object based on its runtime
     * class. If the runtime class does not correspond to any of the known
     * types then returns -1. This can happen for tags
     * that are not handled at this store-agnostic level.
     */
    protected int type(Object o) {
        if (o instanceof ClassMetaData)
            return TYPE_META;
        if (o instanceof QueryMetaData)
            return TYPE_QUERY;
        if (o instanceof SequenceMetaData)
            return TYPE_SEQ;
        if (o instanceof ClassQueries)
            return TYPE_CLASS_QUERIES;
        if (o instanceof ClassSeqs)
            return TYPE_CLASS_SEQS;
        return -1;
    }

    /**
     * Serialize namespace attributes
     */
    private void serializeNamespaceAttributes()
        throws SAXException {
        addAttribute("xmlns", "http://java.sun.com/xml/ns/persistence/orm");
        addAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
        addAttribute("xsi:schemaLocation",
            "http://java.sun.com/xml/ns/persistence/orm orm_2_0.xsd");
        addAttribute("version", "2.0");
    }

    /**
     * Serialize unknown mapping element at system level.
     */
    protected void serializeSystemMappingElement(Object obj)
        throws SAXException {
    }

    /**
     * Serialize query metadata.
     */
    private void serializeQuery(QueryMetaData meta)
        throws SAXException {
        if (!_annos && meta.getSourceType() == SourceTracker.SRC_ANNOTATIONS)
            return;

        Log log = getLog();
        if (log.isInfoEnabled()) {
            if (meta.getSourceScope() instanceof Class)
                log.info(_loc.get("ser-cls-query",
                    meta.getSourceScope(), meta.getName()));
            else
                log.info(_loc.get("ser-query", meta.getName()));
        }

        addComments(meta);
        addAttribute("name", meta.getName());
        addAttribute("query", meta.getQueryString());
        if (QueryLanguages.LANG_SQL.equals(meta.getLanguage())) {
            if (meta.getResultType() != null)
                addAttribute("result-class", meta.getResultType().getName());
            startElement("named-native-query");
            serializeQueryHints(meta);
            endElement("named-native-query");
        } else {
            startElement("named-query");
            serializeQueryHints(meta);
            endElement("named-query");
        }
    }

    /**
     * Serialize query hints.
     */
    private void serializeQueryHints(QueryMetaData meta)
        throws SAXException {
        String[] hints = meta.getHintKeys();
        Object[] values = meta.getHintValues();
        for (int i = 0; i < hints.length; i++) {
            addAttribute("name", hints[i]);
            addAttribute("value", String.valueOf(values[i]));
            startElement("query-hint");
            endElement("query-hint");
        }
    }

    /**
     * Serialize sequence metadata.
     */
    protected void serializeSequence(SequenceMetaData meta)
        throws SAXException {
        if (!_annos && meta.getSourceType() == SourceTracker.SRC_ANNOTATIONS)
            return;

        Log log = getLog();
        if (log.isInfoEnabled())
            log.info(_loc.get("ser-sequence", meta.getName()));

        addComments(meta);
        addAttribute("name", meta.getName());

        // parse out the datastore sequence name, if any
        String plugin = meta.getSequencePlugin();
        String clsName = Configurations.getClassName(plugin);
        String props = Configurations.getProperties(plugin);
        String ds = null;
        if (props != null) {
            Properties map = Configurations.parseProperties(props);
            ds = (String) map.remove("Sequence");
            if (ds != null) {
                props = Configurations.serializeProperties(map);
                plugin = Configurations.getPlugin(clsName, props);
            }
        }

        if (ds != null)
            addAttribute("sequence-name", ds);
        else if (plugin != null && !SequenceMetaData.IMPL_NATIVE.equals
            (plugin))
            addAttribute("sequence-name", plugin);
        if (meta.getInitialValue() != 0 && meta.getInitialValue() != -1)
            addAttribute("initial-value",
                String.valueOf(meta.getInitialValue()));
        if (meta.getAllocate() != 50 && meta.getAllocate() != -1)
            addAttribute("allocation-size",
                String.valueOf(meta.getAllocate()));

        startElement("sequence-generator");
        endElement("sequence-generator");
    }

    /**
     * Serialize class metadata.
     */
    protected void serializeClass(ClassMetaData meta, boolean access)
        throws SAXException {
        if (!_annos && meta.getSourceType() == SourceTracker.SRC_ANNOTATIONS)
            return;

        Log log = getLog();
        if (log.isInfoEnabled())
            log.info(_loc.get("ser-class", meta));

        addComments(meta);
        addAttribute("class", getClassName(meta.getDescribedType().
            getName()));

        if (isMetaDataMode()
            && !meta.getTypeAlias().equals(ClassUtil.getClassName(meta.
            getDescribedType())))
            addAttribute("name", meta.getTypeAlias());

        String name = getEntityElementName(meta);
        if (isMetaDataMode())
            addClassAttributes(meta, access);
        if (isMappingMode())
            addClassMappingAttributes(meta);

        startElement(name);
        if (isMappingMode())
            serializeClassMappingContent(meta);
        if (isMetaDataMode())
            serializeIdClass(meta);
        if (isMappingMode())
            serializeInheritanceContent(meta);

        if (isMappingMode()) {
            List<SequenceMetaData> seqs = (_seqs == null) ? null : _seqs.get
                (meta.getDescribedType().getName());
            if (seqs != null) {
                serializationSort(seqs);
                for (Object seq : seqs) {
                    serializeSequence((SequenceMetaData) seq);
                }
            }
        }

        if (isQueryMode()) {
            List queries = (_queries == null) ? null : _queries.get
                (meta.getDescribedType().getName());
            if (queries != null) {
                serializationSort(queries);
                for (Object query : queries) {
                    serializeQuery((QueryMetaData) query);
                }
            }
            if (isMappingMode())
                serializeQueryMappings(meta);
        }

        List<FieldMetaData> fields = new ArrayList<>(Arrays.asList
            (meta.getDefinedFieldsInListingOrder()));
        Collections.sort(fields, new FieldComparator());

        // serialize attr-override
        if (isMappingMode()) {
            FieldMetaData fmd;
            FieldMetaData orig;
            for (Iterator<FieldMetaData> it = fields.iterator(); it.hasNext();)
            {
                fmd = it.next();
                if (meta.getDefinedSuperclassField(fmd.getName()) == null)
                    continue;
                orig = meta.getPCSuperclassMetaData().getField(fmd.getName());
                if (serializeAttributeOverride(fmd, orig))
                    serializeAttributeOverrideContent(fmd, orig);
                it.remove();
            }
        }

        if (fields.size() > 0 && (isMetaDataMode() || isMappingMode())) {
            startElement("attributes");
            FieldMetaData orig;
            for (FieldMetaData fmd : fields) {
                if (fmd.getDeclaringType() != fmd.getDefiningMetaData().
                    getDescribedType()) {
                    orig = fmd.getDeclaringMetaData().getDeclaredField
                        (fmd.getName());
                } else
                    orig = null;
                serializeField(fmd, orig);
            }
            endElement("attributes");
        }
        endElement(name);
    }

    /**
     * Return the entity element name.
     */
    private static String getEntityElementName(ClassMetaData meta) {
        switch (getEntityTag(meta)) {
            case ENTITY:
                return "entity";
            case EMBEDDABLE:
                return "embeddable";
            case MAPPED_SUPERCLASS:
                return "mapped-superclass";
            default:
                throw new IllegalStateException();
        }
    }

    /**
     * Return the MetaDataTag for the given class meta data.
     */
    private static MetaDataTag getEntityTag(ClassMetaData meta) {
        // @Embeddable classes can't declare Id fields
        if (meta.isEmbeddedOnly() && meta.getPrimaryKeyFields().length == 0)
            return MetaDataTag.EMBEDDABLE;
        if (meta.isMapped())
            return MetaDataTag.ENTITY;
        return MetaDataTag.MAPPED_SUPERCLASS;
    }

    /**
     * Set class attributes.
     *
     * @param access whether to write access
     */
    private void addClassAttributes(ClassMetaData meta, boolean access) {
        if (!access)
            return;
        int def = getConfiguration().getMetaDataRepositoryInstance().
            getMetaDataFactory().getDefaults().getDefaultAccessType();
        if (AccessCode.isField(meta.getAccessType())
            && AccessCode.isProperty(def))
            addAttribute("access", "FIELD");
        else if (AccessCode.isProperty(meta.getAccessType())
            && AccessCode.isField(def))
            addAttribute("access", "PROPERTY");
    }

    /**
     * Add mapping attributes for the given class. Does nothing by default
     */
    protected void addClassMappingAttributes(ClassMetaData mapping)
        throws SAXException {
    }

    /**
     * Serialize id-class.
     */
    private void serializeIdClass(ClassMetaData meta)
        throws SAXException {
        if (meta.getIdentityType() != ClassMetaData.ID_APPLICATION
            || meta.isOpenJPAIdentity())
            return;

        ClassMetaData sup = meta.getPCSuperclassMetaData();
        Class<?> oid = meta.getObjectIdType();
        if (oid != null && (sup == null || oid != sup.getObjectIdType())) {
            addAttribute("class", getClassName(oid.getName()));
            startElement("id-class");
            endElement("id-class");
        }
    }

    /**
     * Serialize class mapping content. Does nothing by default.
     */
    protected void serializeClassMappingContent(ClassMetaData mapping)
        throws SAXException {
    }

    /**
     * Serialize inheritance content. Does nothing by default.
     */
    protected void serializeInheritanceContent(ClassMetaData mapping)
        throws SAXException {
    }

    /**
     * Serialize query mappings. Does nothing by default.
     */
    protected void serializeQueryMappings(ClassMetaData meta)
        throws SAXException {
    }

    /**
     * Serialize the given field.
     */
    private void serializeField(FieldMetaData fmd, FieldMetaData orig)
        throws SAXException {
        if (fmd.getManagement() != FieldMetaData.MANAGE_PERSISTENT
            && !fmd.isExplicit())
            return;

        addComments(fmd);
        addAttribute("name", fmd.getName());

        String strategy = null;
        PersistenceStrategy strat = getStrategy(fmd);
        ValueMetaData cascades = null;
        if (fmd.isPrimaryKey() && strat == PersistenceStrategy.EMBEDDED)
            strategy = "embedded-id";
        else if (fmd.isPrimaryKey())
            strategy = "id";
        else if (fmd.isVersion())
            strategy = "version";
        else {
            switch (strat) {
                case TRANSIENT:
                    strategy = "transient";
                    break;
                case BASIC:
                    if (isMetaDataMode())
                        addBasicAttributes(fmd);
                    strategy = "basic";
                    break;
                case EMBEDDED:
                    strategy = "embedded";
                    break;
                case MANY_ONE:
                    if (isMetaDataMode())
                        addManyToOneAttributes(fmd);
                    strategy = "many-to-one";
                    cascades = fmd;
                    break;
                case ONE_ONE:
                    if (isMetaDataMode())
                        addOneToOneAttributes(fmd);
                    strategy = "one-to-one";
                    cascades = fmd;
                    break;
                case ONE_MANY:
                    if (isMetaDataMode())
                        addOneToManyAttributes(fmd);
                    strategy = "one-to-many";
                    cascades = fmd.getElement();
                    break;
                case MANY_MANY:
                    if (isMetaDataMode())
                        addManyToManyAttributes(fmd);
                    strategy = "many-to-many";
                    cascades = fmd.getElement();
                    break;
                case ELEM_COLL:
                    if (isMetaDataMode())
                        addElementCollectionAttributes(fmd);
                    strategy = "element-collection";
                    break;
            }
            if (isMappingMode())
                addStrategyMappingAttributes(fmd);
        }
        if (isMappingMode(fmd))
            addFieldMappingAttributes(fmd, orig);

        startElement(strategy);
        if (fmd.getOrderDeclaration() != null) {
            startElement("order-by");
            if (!(Order.ELEMENT + " asc").equals(fmd.getOrderDeclaration()))
                addText(fmd.getOrderDeclaration());
            endElement("order-by");
        } else if (isMappingMode(fmd)) {
            serializeOrderColumn(fmd);
        }
        if (isMappingMode() && fmd.getKey().getValueMappedBy() != null) {
            FieldMetaData mapBy = fmd.getKey().getValueMappedByMetaData();
            if (!mapBy.isPrimaryKey() ||
                mapBy.getDefiningMetaData().getPrimaryKeyFields().length != 1) {
                addAttribute("name", fmd.getKey().getValueMappedBy());
            }
            startElement("map-key");
            endElement("map-key");
        }
        if (isMappingMode(fmd))
            serializeFieldMappingContent(fmd, strat);
        if (cascades != null && isMetaDataMode())
            serializeCascades(cascades);
        if (isMappingMode() && strat == PersistenceStrategy.EMBEDDED) {
            ClassMetaData meta = fmd.getEmbeddedMetaData();
            ClassMetaData owner = getConfiguration().
                getMetaDataRepositoryInstance().getMetaData
                (meta.getDescribedType(), meta.getEnvClassLoader(), true);
            FieldMetaData eorig;
            for (FieldMetaData efmd : meta.getFields()) {
                eorig = owner.getField(efmd.getName());
                if (serializeAttributeOverride(efmd, eorig))
                    serializeAttributeOverrideContent(efmd, eorig);
            }
        }
        endElement(strategy);
    }

    /**
     * Add mapping attributes for the given field. Does nothing by default.
     */
    protected void addFieldMappingAttributes(FieldMetaData fmd,
        FieldMetaData orig)
        throws SAXException {
    }

    /**
     * Always returns false by default.
     */
    protected boolean serializeAttributeOverride(FieldMetaData fmd,
        FieldMetaData orig) {
        return false;
    }

    /**
     * Serialize attribute override content.
     */
    private void serializeAttributeOverrideContent(FieldMetaData fmd,
        FieldMetaData orig)
        throws SAXException {
        addAttribute("name", fmd.getName());
        startElement("attribute-override");
        serializeAttributeOverrideMappingContent(fmd, orig);
        endElement("attribute-override");
    }

    /**
     * Serialize attribute override mapping content. Does nothing by default,
     */
    protected void serializeAttributeOverrideMappingContent
        (FieldMetaData fmd, FieldMetaData orig)
        throws SAXException {
    }

    /**
     * Serialize cascades.
     */
    private void serializeCascades(ValueMetaData vmd)
        throws SAXException {
        Collection<String> cascades = null;
        if (vmd.getCascadePersist() == ValueMetaData.CASCADE_IMMEDIATE) {
            if (cascades == null)
                cascades = new ArrayList<>();
            cascades.add("cascade-persist");
        }
        if (vmd.getCascadeAttach() == ValueMetaData.CASCADE_IMMEDIATE) {
            if (cascades == null)
                cascades = new ArrayList<>();
            cascades.add("cascade-merge");
        }
        if (vmd.getCascadeDelete() == ValueMetaData.CASCADE_IMMEDIATE) {
            if (cascades == null)
                cascades = new ArrayList<>();
            cascades.add("cascade-remove");
        }
        if (vmd.getCascadeRefresh() == ValueMetaData.CASCADE_IMMEDIATE) {
            if (cascades == null)
                cascades = new ArrayList<>();
            cascades.add("cascade-refresh");
        }
        if (vmd.getCascadeDetach() == ValueMetaData.CASCADE_IMMEDIATE) {
            if (cascades == null)
                cascades = new ArrayList<>();
            cascades.add("cascade-detach");
        }
        if (cascades != null && cascades.size() == 5) // ALL
        {
            cascades.clear();
            cascades.add("cascade-all");
        }
        if (cascades != null) {
            startElement("cascade");
            for (String cascade : cascades) {
                startElement(cascade);
                endElement(cascade);
            }
            endElement("cascade");
        }
    }

    /**
     * Return the serialized strategy name.
     */
    protected PersistenceStrategy getStrategy(FieldMetaData fmd) {
        if (fmd.getManagement() == FieldMetaData.MANAGE_NONE)
            return PersistenceStrategy.TRANSIENT;

        if (fmd.isSerialized()
            || fmd.getDeclaredType() == byte[].class
            || fmd.getDeclaredType() == Byte[].class
            || fmd.getDeclaredType() == char[].class
            || fmd.getDeclaredType() == Character[].class)
            return PersistenceStrategy.BASIC;

        FieldMetaData mappedBy;
        switch (fmd.getDeclaredTypeCode()) {
            case JavaTypes.PC:
                if (fmd.isEmbedded())
                    return PersistenceStrategy.EMBEDDED;
                if (fmd.getMappedBy() != null)
                    return PersistenceStrategy.ONE_ONE;
                FieldMetaData[] inverses = fmd.getInverseMetaDatas();
                if (inverses.length == 1 &&
                    inverses[0].getTypeCode() == JavaTypes.PC &&
                    inverses[0].getMappedByMetaData() == fmd) {
                    return PersistenceStrategy.ONE_ONE;
                }
                return PersistenceStrategy.MANY_ONE;
            case JavaTypes.ARRAY:
            case JavaTypes.COLLECTION:
            case JavaTypes.MAP:
                if (fmd.isElementCollection())
                    return PersistenceStrategy.ELEM_COLL;
                mappedBy = fmd.getMappedByMetaData();
                if (mappedBy == null || mappedBy.getTypeCode() != JavaTypes.PC)
                    return PersistenceStrategy.MANY_MANY;
                return PersistenceStrategy.ONE_MANY;
            case JavaTypes.OID:
                return PersistenceStrategy.EMBEDDED;
            default:
                return PersistenceStrategy.BASIC;
        }
    }

    /**
     * Add basic attributes.
     */
    private void addBasicAttributes(FieldMetaData fmd)
        throws SAXException {
        if (!fmd.isInDefaultFetchGroup())
            addAttribute("fetch", "LAZY");
        if (fmd.getNullValue() == FieldMetaData.NULL_EXCEPTION)
            addAttribute("optional", "false");
    }

    /**
     * Add many-to-one attributes.
     */
    private void addManyToOneAttributes(FieldMetaData fmd)
        throws SAXException {
        if (!fmd.isInDefaultFetchGroup())
            addAttribute("fetch", "LAZY");
        if (fmd.getNullValue() == FieldMetaData.NULL_EXCEPTION)
            addAttribute("optional", "false");
    }

    /**
     * Add one-to-one attributes.
     */
    private void addOneToOneAttributes(FieldMetaData fmd)
        throws SAXException {
        if (!fmd.isInDefaultFetchGroup())
            addAttribute("fetch", "LAZY");
        if (fmd.getNullValue() == FieldMetaData.NULL_EXCEPTION)
            addAttribute("optional", "false");
    }

    /**
     * Add one-to-many attributes.
     */
    private void addOneToManyAttributes(FieldMetaData fmd)
        throws SAXException {
        if (fmd.isInDefaultFetchGroup())
            addAttribute("fetch", "EAGER");
        addTargetEntityAttribute(fmd);
    }

    /**
     * Add many-to-many attributes.
     */
    private void addManyToManyAttributes(FieldMetaData fmd)
        throws SAXException {
        if (fmd.isInDefaultFetchGroup())
            addAttribute("fetch", "EAGER");
        addTargetEntityAttribute(fmd);
    }

    /**
     * Add element-collection attributes.
     */
    private void addElementCollectionAttributes(FieldMetaData fmd)
        throws SAXException {
        if (fmd.isInDefaultFetchGroup())
            addAttribute("fetch", "EAGER");
        addTargetEntityAttribute(fmd);
    }

    /**
     * Add a target-entity attribute to collection and map fields that do
     * not use generics.
     */
    private void addTargetEntityAttribute(FieldMetaData fmd)
        throws SAXException {
        Member member = fmd.getBackingMember();
        Class[] types;
        if (member instanceof Field)
            types = JavaVersions.getParameterizedTypes((Field) member);
        else if (member instanceof Method)
            types = JavaVersions.getParameterizedTypes((Method) member);
        else
            types = new Class[0];

        switch (fmd.getDeclaredTypeCode()) {
            case JavaTypes.COLLECTION:
                if (types.length != 1)
                    addAttribute("target-entity", fmd.getElement().
                        getDeclaredType().getName());
                break;
            case JavaTypes.MAP:
                if (types.length != 2)
                    addAttribute("target-entity", fmd.getElement().
                        getDeclaredType().getName());
                break;
        }
    }

    /**
     * Serialize field mapping content; this will be called before
     * {@link #serializeValueMappingContent}. Does nothing by default.
     */
    protected void serializeFieldMappingContent(FieldMetaData fmd,
        PersistenceStrategy strategy)
        throws SAXException {
    }

    /**
     * Set mapping attributes for strategy. Sets mapped-by by default.
     */
    protected void addStrategyMappingAttributes(FieldMetaData fmd)
        throws SAXException {
        if (fmd.getMappedBy() != null)
            addAttribute("mapped-by", fmd.getMappedBy());
    }

    /**
     * Order column is not processed as meta data, instead it
     * can be processed as mapping data if in mapping mode.
     */
    protected void serializeOrderColumn(FieldMetaData fmd)
        throws SAXException {
    }

    /**
     * Represents ordered set of {@link SequenceMetaData}s with a
     * common class scope.
     *
     * @author Stephen Kim
     * @author Pinaki Poddar
     */
    private static class ClassSeqs
        implements SourceTracker, Comparable<ClassSeqs>,
        Comparator<SequenceMetaData> {

        private final SequenceMetaData[] _seqs;

        public ClassSeqs(List<SequenceMetaData> seqs) {
            if (seqs == null || seqs.isEmpty())
                throw new InternalException();

            _seqs = (SequenceMetaData[]) seqs.toArray
                (new SequenceMetaData[seqs.size()]);
            Arrays.sort(_seqs, this);
        }

        public SequenceMetaData[] getSequences() {
            return _seqs;
        }

        /**
         * Compare sequence metadata on name.
         */
        @Override
        public int compare(SequenceMetaData o1, SequenceMetaData o2) {
            return o1.getName().compareTo(o2.getName());
        }

        @Override
        public File getSourceFile() {
            return _seqs[0].getSourceFile();
        }

        @Override
        public Object getSourceScope() {
            return _seqs[0].getSourceScope();
        }

        @Override
        public int getSourceType() {
            return _seqs[0].getSourceType();
        }

        @Override
        public String getResourceName() {
            return _seqs[0].getResourceName();
        }

        @Override
        public int getLineNumber() {
            return _seqs[0].getLineNumber();
        }

        @Override
        public int getColNumber() {
            return _seqs[0].getColNumber();
        }

        @Override
        public int compareTo(ClassSeqs other) {
            if (other == this)
                return 0;
            if (other == null)
                return -1;
            Class scope = (Class) getSourceScope();
            Class oscope = (Class) other.getSourceScope();
            return scope.getName().compareTo(oscope.getName());
        }
    }

    /**
     * Represents ordered set of {@link QueryMetaData}s with a
     * common class scope.
     *
     * @author Stephen Kim
     * @author Pinaki Poddar
     */
    private static class ClassQueries
        implements SourceTracker, Comparable<ClassQueries>,
        Comparator<QueryMetaData> {

        private final QueryMetaData[] _queries;

        public ClassQueries(List<QueryMetaData> queries) {
            if (queries == null || queries.isEmpty())
                throw new InternalException();

            _queries = (QueryMetaData[]) queries.toArray
                (new QueryMetaData[queries.size()]);
            Arrays.sort(_queries, this);
        }

        public QueryMetaData[] getQueries() {
            return _queries;
        }

        /**
         * Compare query metadata. Normal queries appear before native queries.
         * If the given queries use same language, then their names are
         * compared.
         */
        @Override
        public int compare(QueryMetaData o1, QueryMetaData o2) {
            // normal queries before native
            if (!Objects.equals(o1.getLanguage(), o2.getLanguage())) {
                if (QueryLanguages.LANG_SQL.equals(o1.getLanguage()))
                    return 1;
                else
                    return -1;
            }
            return o1.getName().compareTo(o2.getName());
        }

        @Override
        public File getSourceFile() {
            return _queries[0].getSourceFile();
        }

        @Override
        public Object getSourceScope() {
            return _queries[0].getSourceScope();
        }

        @Override
        public int getSourceType() {
            return _queries[0].getSourceType();
        }

        @Override
        public String getResourceName() {
            return _queries[0].getResourceName();
        }

        @Override
        public int getLineNumber() {
            return _queries[0].getLineNumber();
        }

        @Override
        public int getColNumber() {
            return _queries[0].getColNumber();
        }

        @Override
        public int compareTo(ClassQueries other) {
            if (other == this)
                return 0;
            if (other == null)
                return -1;
            Class scope = (Class) getSourceScope();
            Class oscope = (Class) other.getSourceScope();
            return scope.getName().compareTo(oscope.getName());
        }
    }

    /**
     * Compares clases, sequences, and queries to order them for serialization.
     * Places sequences first, then classes, then queries. Sequences and
     * queries are ordered alphabetically by name. Classes are placed in
     * listing order, in inheritance order within that, and in alphabetical
     * order within that.
     *
     * @author Stephen Kim
     */
    protected class SerializationComparator
        extends MetaDataInheritanceComparator {

        
        private static final long serialVersionUID = 1L;

        @Override
        public int compare(Object o1, Object o2) {
            if (o1 == o2)
                return 0;
            if (o1 == null)
                return 1;
            if (o2 == null)
                return -1;

            int t1 = type(o1);
            int t2 = type(o2);
            if (t1 != t2)
                return t1 - t2;

            switch (t1) {
                case TYPE_META:
                    return compare((ClassMetaData) o1, (ClassMetaData) o2);
                case TYPE_QUERY:
                    return compare((QueryMetaData) o1, (QueryMetaData) o2);
                case TYPE_SEQ:
                    return compare((SequenceMetaData) o1,
                        (SequenceMetaData) o2);
                case TYPE_CLASS_QUERIES:
                    return ((Comparable) o1).compareTo(o2);
                case TYPE_CLASS_SEQS:
                    return ((Comparable) o1).compareTo(o2);
                default:
                    return compareUnknown(o1, o2);
            }
        }

        /**
         * Compare two unrecognized elements of the same type. Throws
         * exception by default.
         */
        protected int compareUnknown(Object o1, Object o2) {
            throw new InternalException();
        }

        /**
         * Compare between two class metadata.
         */
        private int compare(ClassMetaData o1, ClassMetaData o2) {
            int li1 = o1.getListingIndex();
            int li2 = o2.getListingIndex();
            if (li1 == -1 && li2 == -1) {
                MetaDataTag t1 = getEntityTag(o1);
                MetaDataTag t2 = getEntityTag(o2);
                if (t1.compareTo(t2) != 0)
                    return t1.compareTo(t2);
                int inher = super.compare(o1, o2);
                if (inher != 0)
                    return inher;
                return o1.getDescribedType().getName().compareTo
                    (o2.getDescribedType().getName());
            }

            if (li1 == -1)
                return 1;
            if (li2 == -1)
                return -1;
            return li1 - li2;
        }

        /**
         * Compare query metadata.
         */
        private int compare(QueryMetaData o1, QueryMetaData o2) {
            // normal queries before native
            if (!Objects.equals(o1.getLanguage(), o2.getLanguage())) {
                if (QueryLanguages.LANG_SQL.equals(o1.getLanguage()))
                    return 1;
                else
                    return -1;
            }
            return o1.getName().compareTo(o2.getName());
        }

        /**
         * Compare sequence metadata.
         */
        private int compare(SequenceMetaData o1, SequenceMetaData o2) {
            return o1.getName().compareTo(o2.getName());
        }
    }

    /**
     * Sorts fields according to listing order, then XSD strategy order,
     * then name order.
     */
    private class FieldComparator
        implements Comparator {

        @Override
        public int compare(Object o1, Object o2) {
            FieldMetaData fmd1 = (FieldMetaData) o1;
            FieldMetaData fmd2 = (FieldMetaData) o2;
            if (fmd1.isPrimaryKey()) {
                if (fmd2.isPrimaryKey())
                    return fmd1.compareTo(fmd2);
                return -1;
            }
            if (fmd2.isPrimaryKey())
                return 1;

            if (fmd1.isVersion()) {
                if (fmd2.isVersion())
                    return compareListingOrder(fmd1, fmd2);
                return getStrategy(fmd2) == PersistenceStrategy.BASIC ? 1 : -1;
			}
			if (fmd2.isVersion())
                return getStrategy(fmd1) == PersistenceStrategy.BASIC ? -1 : 1;

            int stcmp = getStrategy(fmd1).compareTo(getStrategy(fmd2));
            if (stcmp != 0)
                return stcmp;
            return compareListingOrder(fmd1, fmd2);
        }

        private int compareListingOrder(FieldMetaData fmd1, FieldMetaData fmd2){
            int lcmp = fmd1.getListingIndex() - fmd2.getListingIndex();
            if (lcmp != 0)
                return lcmp;
            return fmd1.compareTo(fmd2);
		}
	}

    /**
     * Returns the stored ClassMetaData
     */
    public Map<String, ClassMetaData> getClassMetaData() {
        return _metas;
    }
}
