/*
 * 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 java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;

import jakarta.persistence.AttributeOverride;
import jakarta.persistence.Basic;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Embedded;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapKey;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.NamedNativeQuery;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.OrderBy;
import jakarta.persistence.QueryHint;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Transient;
import jakarta.persistence.Version;

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.SourceTracker;
import org.apache.openjpa.lib.util.ClassUtil;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.JavaVersions;
import org.apache.openjpa.lib.util.Localizer;
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.MetaDataModes;
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;


//@todo: javadocs

/**
 * Serializes persistence metadata as annotations.
 * 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 1.0.0
 * @author Steve Kim
 * @author Gokhan Ergul
 */
public class AnnotationPersistenceMetaDataSerializer
    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
        (AnnotationPersistenceMetaDataSerializer.class);

    private Log _log = null;

    private final OpenJPAConfiguration _conf;
    private Map<String, ClassMetaData> _metas = null;
    private Map<String, List> _queries = null;
    private Map<String, List> _seqs = null;
    private int _mode = MetaDataModes.MODE_NONE;
    private SerializationComparator _comp = null;

    private Map<ClassMetaData, List<AnnotationBuilder>> _clsAnnos = null;
    private Map<FieldMetaData, List<AnnotationBuilder>> _fldAnnos = null;
    private Map<SequenceMetaData, List<AnnotationBuilder>> _seqAnnos = null;
    private Map<QueryMetaData, List<AnnotationBuilder>> _qryAnnos = null;

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

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

    /**
     * The log to write to.
     */
    public Log getLog() {
        return _log;
    }

    /**
     * The log to write to.
     */
    public void setLog(Log log) {
        _log = log;
    }

    /**
     * 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 == MetaDataModes.MODE_NONE)
            setMode(MetaDataModes.MODE_NONE);
        else if (on)
            setMode(_mode | mode);
        else
            setMode(_mode & ~mode);
    }

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

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

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

    /**
     * Convenience method for interpreting {@link #getMode}. Takes into
     * account whether mapping information is loaded for the given instance.
     * OPENJPA-1360 - Allow @Column attributes when meta.isEmbeddedOnly()
     */
    protected boolean isMappingMode(ClassMetaData meta) {
        return isMappingMode() && (meta.getSourceMode()
            & MetaDataModes.MODE_MAPPING) != 0
            && (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 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 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 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 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();
    }

    /**
     * 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 == MetaDataModes.MODE_QUERY || _metas == null
                || !_metas.containsKey(entry.getKey()))
                all.add(new ClassQueries((List<QueryMetaData>)
                    entry.getValue()));
        }
    }

    /**
     * Creates a new annotation builder for the specified annotation type.
     */
    protected AnnotationBuilder newAnnotationBuilder(
        Class<? extends Annotation> annType) {
        return new AnnotationBuilder(annType);
    }

    protected void addAnnotation(AnnotationBuilder ab, Object meta) {
        if (meta instanceof ClassMetaData)
            addAnnotation(ab, (ClassMetaData) meta);
        else if (meta instanceof FieldMetaData)
            addAnnotation(ab, (FieldMetaData) meta);
        else if (meta instanceof SequenceMetaData)
            addAnnotation(ab, (SequenceMetaData) meta);
        else if (meta instanceof QueryMetaData)
            addAnnotation(ab, (QueryMetaData) meta);
    }

    /**
     * Add an annotation builder to list of builders for the specified
     * class metadata.
     */
    protected void addAnnotation(AnnotationBuilder ab, ClassMetaData meta) {
        if (_clsAnnos == null)
            _clsAnnos = new HashMap<>();
        List<AnnotationBuilder> list = _clsAnnos.computeIfAbsent(meta, k -> new ArrayList<>());
        list.add(ab);
    }

    /**
     * Add an annotation builder to list of builders for the specified
     * field metadata.
     */
    protected void addAnnotation(AnnotationBuilder ab, FieldMetaData meta) {
        if (_fldAnnos == null)
            _fldAnnos = new HashMap<>();
        List<AnnotationBuilder> list = _fldAnnos.computeIfAbsent(meta, k -> new ArrayList<>());
        list.add(ab);
    }

    /**
     * Add an annotation builder to list of builders for the specified
     * sequence metadata.
     */
    protected void addAnnotation(AnnotationBuilder ab, SequenceMetaData meta) {
        if (_seqAnnos == null)
            _seqAnnos = new HashMap<>();
        List<AnnotationBuilder> list = _seqAnnos.computeIfAbsent(meta, k -> new ArrayList<>());
        list.add(ab);
    }

    /**
     * Add an annotation builder to list of builders for the specified
     * query metadata.
     */
    protected void addAnnotation(AnnotationBuilder ab, QueryMetaData meta) {
        if (_qryAnnos == null)
            _qryAnnos = new HashMap<>();
        List<AnnotationBuilder> list = _qryAnnos.computeIfAbsent(meta, k -> new ArrayList<>());
        list.add(ab);
    }

    /**
     * Creates an an annotation builder for the specified class metadata
     * and adds it to list of builders.
     */
    protected AnnotationBuilder addAnnotation(
        Class<? extends Annotation> annType, ClassMetaData meta) {
        AnnotationBuilder ab = newAnnotationBuilder(annType);
        if (meta == null)
            return ab;
        addAnnotation(ab, meta);
        return ab;
    }

    /**
     * Creates an an annotation builder for the specified class metadata
     * and adds it to list of builders.
     */
    protected AnnotationBuilder addAnnotation(
        Class<? extends Annotation> annType, FieldMetaData meta) {
        AnnotationBuilder ab = newAnnotationBuilder(annType);
        if (meta == null)
            return ab;
        addAnnotation(ab, meta);
        return ab;
    }

    /**
     * Creates an an annotation builder for the specified class metadata
     * and adds it to list of builders.
     */
    protected AnnotationBuilder addAnnotation(
        Class<? extends Annotation> annType, SequenceMetaData meta) {
        AnnotationBuilder ab = newAnnotationBuilder(annType);
        if (meta == null)
            return ab;
        addAnnotation(ab, meta);
        return ab;
    }

    /**
     * Creates an an annotation builder for the specified class metadata
     * and adds it to list of builders.
     */
    protected AnnotationBuilder addAnnotation(
        Class<? extends Annotation> annType, QueryMetaData meta) {
        AnnotationBuilder ab = newAnnotationBuilder(annType);
        if (meta == null)
            return ab;
        addAnnotation(ab, meta);
        return ab;
    }

    protected void serialize(Collection objects) {
        for (Object obj : objects) {
            int type = type(obj);
            switch (type) {
                case TYPE_META:
                    serializeClass((ClassMetaData) obj);
                    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;
            }
        }
    }

    /**
     * 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 unknown mapping element at system level.
     */
    protected void serializeSystemMappingElement(Object obj) {
    }

    /**
     * Serialize query metadata.
     */
    private void serializeQuery(QueryMetaData meta) {
        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()));
        }

        Class<? extends Annotation> ann =
            QueryLanguages.LANG_SQL.equals(meta.getLanguage()) ?
            NamedNativeQuery.class : NamedQuery.class;
        AnnotationBuilder abQry = addAnnotation(ann, meta);
        abQry.add("name", meta.getName());
        abQry.add("query", meta.getQueryString());
        if (QueryLanguages.LANG_SQL.equals(meta.getLanguage())) {
            if (meta.getResultType() != null)
                abQry.add("resultClass", meta.getResultType());
        }
        serializeQueryHints(meta, abQry);
    }

    /**
     * Serialize query hints.
     */
    private void serializeQueryHints(QueryMetaData meta, AnnotationBuilder ab) {
        String[] hints = meta.getHintKeys();
        Object[] values = meta.getHintValues();
        for (int i = 0; i < hints.length; i++) {
            AnnotationBuilder abHint = newAnnotationBuilder(QueryHint.class);
            abHint.add("name", hints[i]);
            abHint.add("value", String.valueOf(values[i]));
            ab.add("hints", abHint);
        }
    }

    /**
     * Serialize sequence metadata.
     */
    protected void serializeSequence(SequenceMetaData meta) {
        Log log = getLog();
        if (log.isInfoEnabled())
            log.info(_loc.get("ser-sequence", meta.getName()));

        AnnotationBuilder ab = addAnnotation(SequenceGenerator.class, meta);
        ab.add("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)
            ab.add("sequenceName", ds);
        else if (plugin != null && !SequenceMetaData.IMPL_NATIVE.equals
            (plugin))
            ab.add("sequenceName", plugin);
        if (meta.getInitialValue() != 0 && meta.getInitialValue() != -1)
            ab.add("initialValue", meta.getInitialValue());
        if (meta.getAllocate() != 50 && meta.getAllocate() != -1)
            ab.add("allocationSize", meta.getAllocate());
        if (meta.getSchema() != null)
            ab.add("schema", meta.getSchema());
        if (meta.getCatalog() != null)
            ab.add("catalog", meta.getCatalog());
    }

    /**
     * Serialize class metadata.
     */
    protected void serializeClass(ClassMetaData meta) {
        Log log = getLog();
        if (log.isInfoEnabled())
            log.info(_loc.get("ser-class", meta));

        AnnotationBuilder abEntity = addAnnotation(
            getEntityAnnotationType(meta), meta);
        if (isMetaDataMode()
            && !meta.getTypeAlias().equals(ClassUtil.getClassName(meta.
            getDescribedType())))
            abEntity.add("name", meta.getTypeAlias());

        if (isMappingMode())
            addClassMappingAnnotations(meta);

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

        if (isMappingMode()) {
            List 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())) {
            FieldMetaData orig;
            for (FieldMetaData fmd : fields) {
                if (fmd.getDeclaringType() != fmd.getDefiningMetaData().
                    getDescribedType()) {
                    orig = fmd.getDeclaringMetaData().getDeclaredField
                        (fmd.getName());
                } else
                    orig = null;
                serializeField(fmd, orig);
            }
        }
    }

    /**
     * Return entity annotation type.
     */
    private static Class<? extends Annotation> getEntityAnnotationType(
        ClassMetaData meta) {
        switch (getEntityTag(meta)) {
            case ENTITY:
                return Entity.class;
            case EMBEDDABLE:
                return Embeddable.class;
            case MAPPED_SUPERCLASS:
                return MappedSuperclass.class;
            default:
                throw new IllegalStateException();
        }
    }

    /**
     * Return field annotation type.
     */
    private static Class<? extends Annotation> getFieldAnnotationType (
        FieldMetaData fmd, PersistenceStrategy strat) {
        Class<? extends Annotation> ann = null;
        if (fmd.isPrimaryKey() && strat == PersistenceStrategy.EMBEDDED)
            ann = EmbeddedId.class;
        else if (fmd.isPrimaryKey())
            ann = Id.class;
        else if (fmd.isVersion())
            ann = Version.class;
        else {
            switch (strat) {
                case TRANSIENT:
                    ann = Transient.class;
                    break;
                case BASIC:
                    ann = Basic.class;
                    break;
                case EMBEDDED:
                    ann = Embedded.class;
                    break;
                case MANY_ONE:
                    ann = ManyToOne.class;
                    break;
                case ONE_ONE:
                    ann = OneToOne.class;
                    break;
                case ONE_MANY:
                    ann = OneToMany.class;
                    break;
                case MANY_MANY:
                    ann = ManyToMany.class;
                    break;
            }
        }
        return ann;
    }

    /**
     * Return the MetaDataTag for the given class meta data.
     */
    private static MetaDataTag getEntityTag(ClassMetaData meta) {
        if (meta.isAbstract())
            return MetaDataTag.MAPPED_SUPERCLASS;
        // @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;
    }

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

    /**
     * Serialize id-class.
     */
    private void serializeIdClass(ClassMetaData meta) {
        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())) {
            AnnotationBuilder ab = addAnnotation(IdClass.class, meta);
            ab.add(null, oid);
        }
    }

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

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

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

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

        PersistenceStrategy strat = getStrategy(fmd);
        ValueMetaData cascades = null;
        AnnotationBuilder ab = addAnnotation(
            getFieldAnnotationType (fmd, strat), fmd);
        if (fmd.isPrimaryKey() && strat == PersistenceStrategy.EMBEDDED)
            ; // noop
        else if (fmd.isPrimaryKey())
            ; // noop
        else if (fmd.isVersion())
            ; // noop
        else {
            switch (strat) {
                case BASIC:
                    if (isMetaDataMode())
                        addBasicAttributes(fmd, ab);
                    break;
                case MANY_ONE:
                    if (isMetaDataMode())
                        addManyToOneAttributes(fmd, ab);
                    cascades = fmd;
                    break;
                case ONE_ONE:
                    if (isMetaDataMode())
                        addOneToOneAttributes(fmd, ab);
                    cascades = fmd;
                    break;
                case ONE_MANY:
                    if (isMetaDataMode())
                        addOneToManyAttributes(fmd, ab);
                    cascades = fmd.getElement();
                    break;
                case MANY_MANY:
                    if (isMetaDataMode())
                        addManyToManyAttributes(fmd, ab);
                    cascades = fmd.getElement();
                    break;
            }
            if (isMappingMode())
                addStrategyMappingAttributes(fmd, ab);
        }
        if (isMappingMode(fmd))
            addFieldMappingAttributes(fmd, orig, ab);

        if (fmd.getOrderDeclaration() != null) {
            if (!(Order.ELEMENT + " asc").equals(fmd.getOrderDeclaration()))
                addAnnotation(OrderBy.class, fmd).
                    add (null, fmd.getOrderDeclaration());
        }
        if (isMappingMode() && fmd.getKey().getValueMappedBy() != null) {
            AnnotationBuilder abMapKey = addAnnotation(MapKey.class, fmd);
            FieldMetaData mapBy = fmd.getKey().getValueMappedByMetaData();
            if (!mapBy.isPrimaryKey() ||
                mapBy.getDefiningMetaData().getPrimaryKeyFields().length != 1) {
                abMapKey.add("name", fmd.getKey().getValueMappedBy());
            }
        }
        if (isMappingMode(fmd))
            serializeFieldMappingContent(fmd, strat, ab);
        if (cascades != null && isMetaDataMode())
            serializeCascades(cascades, ab);
    }

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

    /**
     * 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) {
        AnnotationBuilder ab = addAnnotation(AttributeOverride.class, fmd);
        ab.add("name", fmd.getName());
        serializeAttributeOverrideMappingContent(fmd, orig, ab);
    }

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


    /**
     * Serialize cascades.
     */
    private void serializeCascades(ValueMetaData vmd, AnnotationBuilder ab) {
        EnumSet<CascadeType> cascades = EnumSet.noneOf(CascadeType.class);
        if (vmd.getCascadePersist() == ValueMetaData.CASCADE_IMMEDIATE) {
            cascades.add(CascadeType.PERSIST);
        }
        if (vmd.getCascadeAttach() == ValueMetaData.CASCADE_IMMEDIATE) {
            cascades.add(CascadeType.MERGE);
        }
        if (vmd.getCascadeDelete() == ValueMetaData.CASCADE_IMMEDIATE) {
            cascades.add(CascadeType.REMOVE);
        }
        if (vmd.getCascadeRefresh() == ValueMetaData.CASCADE_IMMEDIATE) {
            cascades.add(CascadeType.REFRESH);
        }
        if (vmd.getCascadeDetach() == ValueMetaData.CASCADE_IMMEDIATE) {
            cascades.add(CascadeType.DETACH);
        }
        if (cascades.size() == 5) // ALL
        {
            cascades.clear();
            cascades.add(CascadeType.ALL);
        }
        if (!cascades.isEmpty()) {
            ab.add("cascade", cascades);
        }
    }

    /**
     * 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:
                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, AnnotationBuilder ab) {
        if (!fmd.isInDefaultFetchGroup())
            ab.add("fetch", FetchType.LAZY);
        if (fmd.getNullValue() == FieldMetaData.NULL_EXCEPTION)
            ab.add("optional", false);
    }

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

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

    /**
     * Add one-to-many attributes.
     */
    private void addOneToManyAttributes(FieldMetaData fmd,
        AnnotationBuilder ab) {
        if (fmd.isInDefaultFetchGroup())
            ab.add("fetch", FetchType.EAGER);
        addTargetEntityAttribute(fmd, ab);
    }

    /**
     * Add many-to-many attributes.
     */
    private void addManyToManyAttributes(FieldMetaData fmd,
        AnnotationBuilder ab) {
        if (fmd.isInDefaultFetchGroup())
            ab.add("fetch", FetchType.EAGER);
        addTargetEntityAttribute(fmd, ab);
    }

    /**
     * Add a target-entity attribute to collection and map fields that do
     * not use generics.
     */
    private void addTargetEntityAttribute(FieldMetaData fmd,
        AnnotationBuilder ab) {
        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)
                    ab.add("targetEntity", fmd.getElement().getDeclaredType());
                break;
            case JavaTypes.MAP:
                if (types.length != 2)
                    ab.add("targetEntity", fmd.getElement().getDeclaredType());
                break;
        }
    }

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

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

    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;
    }

    protected void writeAnnotations(Object meta,
        List<AnnotationBuilder> builders, Map output) {
        List<String> annos = new ArrayList<>();
        for(AnnotationBuilder ab: builders)
            annos.add(ab.toString());
        output.put(meta, annos);
    }

    @Override
    public void serialize(Map output, int flags) throws IOException {
        Collection all = getObjects();
        serialize(all);

        if (_clsAnnos != null)
            for (ClassMetaData meta : _clsAnnos.keySet())
                writeAnnotations(meta, _clsAnnos.get(meta), output);
        if (_fldAnnos != null)
            for (FieldMetaData meta : _fldAnnos.keySet())
                writeAnnotations(meta, _fldAnnos.get(meta), output);
        if (_seqAnnos != null)
            for (SequenceMetaData meta : _seqAnnos.keySet())
                writeAnnotations(meta, _seqAnnos.get(meta), output);
        if (_qryAnnos != null)
            for (QueryMetaData meta : _qryAnnos.keySet())
                writeAnnotations(meta, _qryAnnos.get(meta), output);
    }

    @Override
    public void serialize(File file, int flags) throws IOException {
        try {
            FileWriter out = new FileWriter(AccessController
                .doPrivileged(J2DoPrivHelper.getCanonicalPathAction(file)),
                (flags & APPEND) > 0);
            serialize(out, flags);
            out.close();
        } catch (PrivilegedActionException pae) {
            throw (IOException) pae.getException();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void serialize(Writer out, int flags) throws IOException {
        Map output = new HashMap();
        serialize(output, flags);

        Set<Entry> entrySet = output.entrySet();
        for(Entry entry : entrySet) {
            Object meta = entry.getKey();
            List<String> annos = (List<String>) entry.getValue();

            out.write("--"+meta.toString());
            out.write("\n");
            for(String ann: annos) {
                out.write("\t");
                out.write(ann);
                out.write("\n");
            }
        }
    }

    @Override
    public void serialize(int flags) throws IOException {
        throw new UnsupportedOperationException();
    }

    /**
     * Represents ordered set of
     * {@link org.apache.openjpa.meta.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 org.apache.openjpa.meta.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;
	}
}
