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