/*
 * 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) {
