/*
 * 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 jakarta.persistence.GenerationType.AUTO;
import static org.apache.openjpa.persistence.MetaDataTag.ACCESS;
import static org.apache.openjpa.persistence.MetaDataTag.CACHEABLE;
import static org.apache.openjpa.persistence.MetaDataTag.DATASTORE_ID;
import static org.apache.openjpa.persistence.MetaDataTag.DATA_CACHE;
import static org.apache.openjpa.persistence.MetaDataTag.DEPENDENT;
import static org.apache.openjpa.persistence.MetaDataTag.DETACHED_STATE;
import static org.apache.openjpa.persistence.MetaDataTag.ELEM_DEPENDENT;
import static org.apache.openjpa.persistence.MetaDataTag.ELEM_TYPE;
import static org.apache.openjpa.persistence.MetaDataTag.EMBEDDED_ID;
import static org.apache.openjpa.persistence.MetaDataTag.ENTITY_LISTENERS;
import static org.apache.openjpa.persistence.MetaDataTag.EXCLUDE_DEFAULT_LISTENERS;
import static org.apache.openjpa.persistence.MetaDataTag.EXCLUDE_SUPERCLASS_LISTENERS;
import static org.apache.openjpa.persistence.MetaDataTag.EXTERNALIZER;
import static org.apache.openjpa.persistence.MetaDataTag.EXTERNAL_VALS;
import static org.apache.openjpa.persistence.MetaDataTag.FACTORY;
import static org.apache.openjpa.persistence.MetaDataTag.FETCH_GROUP;
import static org.apache.openjpa.persistence.MetaDataTag.FETCH_GROUPS;
import static org.apache.openjpa.persistence.MetaDataTag.FLUSH_MODE;
import static org.apache.openjpa.persistence.MetaDataTag.GENERATED_VALUE;
import static org.apache.openjpa.persistence.MetaDataTag.ID;
import static org.apache.openjpa.persistence.MetaDataTag.ID_CLASS;
import static org.apache.openjpa.persistence.MetaDataTag.INVERSE_LOGICAL;
import static org.apache.openjpa.persistence.MetaDataTag.KEY_DEPENDENT;
import static org.apache.openjpa.persistence.MetaDataTag.KEY_TYPE;
import static org.apache.openjpa.persistence.MetaDataTag.LOAD_FETCH_GROUP;
import static org.apache.openjpa.persistence.MetaDataTag.LRS;
import static org.apache.openjpa.persistence.MetaDataTag.MANAGED_INTERFACE;
import static org.apache.openjpa.persistence.MetaDataTag.MAPPED_BY_ID;
import static org.apache.openjpa.persistence.MetaDataTag.MAP_KEY;
import static org.apache.openjpa.persistence.MetaDataTag.MAP_KEY_CLASS;
import static org.apache.openjpa.persistence.MetaDataTag.NATIVE_QUERIES;
import static org.apache.openjpa.persistence.MetaDataTag.NATIVE_QUERY;
import static org.apache.openjpa.persistence.MetaDataTag.ORDER_BY;
import static org.apache.openjpa.persistence.MetaDataTag.POST_LOAD;
import static org.apache.openjpa.persistence.MetaDataTag.POST_PERSIST;
import static org.apache.openjpa.persistence.MetaDataTag.POST_REMOVE;
import static org.apache.openjpa.persistence.MetaDataTag.POST_UPDATE;
import static org.apache.openjpa.persistence.MetaDataTag.PRE_PERSIST;
import static org.apache.openjpa.persistence.MetaDataTag.PRE_REMOVE;
import static org.apache.openjpa.persistence.MetaDataTag.PRE_UPDATE;
import static org.apache.openjpa.persistence.MetaDataTag.QUERIES;
import static org.apache.openjpa.persistence.MetaDataTag.QUERY;
import static org.apache.openjpa.persistence.MetaDataTag.READ_ONLY;
import static org.apache.openjpa.persistence.MetaDataTag.SEQ_GENERATOR;
import static org.apache.openjpa.persistence.MetaDataTag.STOREDPROCEDURE_QUERIES;
import static org.apache.openjpa.persistence.MetaDataTag.STOREDPROCEDURE_QUERY;
import static org.apache.openjpa.persistence.MetaDataTag.TYPE;
import static org.apache.openjpa.persistence.MetaDataTag.VERSION;

import java.io.File;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.AccessController;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Stack;
import java.util.TreeSet;
import java.util.UUID;

import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Basic;
import jakarta.persistence.Cacheable;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Convert;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Embedded;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.ExcludeDefaultListeners;
import jakarta.persistence.ExcludeSuperclassListeners;
import jakarta.persistence.FetchType;
import jakarta.persistence.FlushModeType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.Lob;
import jakarta.persistence.LockModeType;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapKey;
import jakarta.persistence.MapKeyClass;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.MapsId;
import jakarta.persistence.NamedNativeQueries;
import jakarta.persistence.NamedNativeQuery;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.NamedStoredProcedureQueries;
import jakarta.persistence.NamedStoredProcedureQuery;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.OrderBy;
import jakarta.persistence.ParameterMode;
import jakarta.persistence.PostLoad;
import jakarta.persistence.PostPersist;
import jakarta.persistence.PostRemove;
import jakarta.persistence.PostUpdate;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreRemove;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.QueryHint;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.StoredProcedureParameter;
import jakarta.persistence.Version;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.event.BeanLifecycleCallbacks;
import org.apache.openjpa.event.LifecycleCallbacks;
import org.apache.openjpa.event.LifecycleEvent;
import org.apache.openjpa.event.MethodLifecycleCallbacks;
import org.apache.openjpa.kernel.QueryLanguages;
import org.apache.openjpa.kernel.jpql.JPQLParser;
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.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.meta.AccessCode;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.DelegatingMetaDataFactory;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.LifecycleMetaData;
import org.apache.openjpa.meta.MetaDataFactory;
import org.apache.openjpa.meta.MetaDataModes;
import org.apache.openjpa.meta.MetaDataRepository;
import org.apache.openjpa.meta.MultiQueryMetaData;
import org.apache.openjpa.meta.Order;
import org.apache.openjpa.meta.QueryMetaData;
import org.apache.openjpa.meta.SequenceMetaData;
import org.apache.openjpa.meta.UpdateStrategies;
import org.apache.openjpa.meta.ValueMetaData;
import org.apache.openjpa.meta.ValueStrategies;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.MetaDataException;
import org.apache.openjpa.util.UnsupportedException;
import org.apache.openjpa.util.UserException;
import static org.apache.openjpa.persistence.MetaDataTag.CONVERT;


/**
 * Persistence annotation metadata parser. Currently does not parse
 * deployment descriptors.
 *
 * @author Abe White
 * @author Steve Kim
 */
public class AnnotationPersistenceMetaDataParser
    implements MetaDataModes {

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

    private static final Map<Class<?>, MetaDataTag> _tags =
        new HashMap<>();

    static {
        _tags.put(Access.class, ACCESS);
        _tags.put(Cacheable.class, CACHEABLE);
        _tags.put(EmbeddedId.class, EMBEDDED_ID);
        _tags.put(EntityListeners.class, ENTITY_LISTENERS);
        _tags.put(ExcludeDefaultListeners.class, EXCLUDE_DEFAULT_LISTENERS);
        _tags.put(ExcludeSuperclassListeners.class,
            EXCLUDE_SUPERCLASS_LISTENERS);
        _tags.put(FlushModeType.class, FLUSH_MODE);
        _tags.put(GeneratedValue.class, GENERATED_VALUE);
        _tags.put(Id.class, ID);
        _tags.put(IdClass.class, ID_CLASS);
        _tags.put(MapKey.class, MAP_KEY);
        _tags.put(MapKeyClass.class, MAP_KEY_CLASS);
        _tags.put(MapsId.class, MAPPED_BY_ID);
        _tags.put(NamedNativeQueries.class, NATIVE_QUERIES);
        _tags.put(NamedNativeQuery.class, NATIVE_QUERY);
        _tags.put(NamedStoredProcedureQueries.class, STOREDPROCEDURE_QUERIES);
        _tags.put(NamedStoredProcedureQuery.class, STOREDPROCEDURE_QUERY);
        _tags.put(NamedQueries.class, QUERIES);
        _tags.put(NamedQuery.class, QUERY);
        _tags.put(OrderBy.class, ORDER_BY);
        _tags.put(PostLoad.class, POST_LOAD);
        _tags.put(PostPersist.class, POST_PERSIST);
        _tags.put(PostRemove.class, POST_REMOVE);
        _tags.put(PostUpdate.class, POST_UPDATE);
        _tags.put(PrePersist.class, PRE_PERSIST);
        _tags.put(PreRemove.class, PRE_REMOVE);
        _tags.put(PreUpdate.class, PRE_UPDATE);
        _tags.put(SequenceGenerator.class, SEQ_GENERATOR);
        _tags.put(Version.class, VERSION);
        _tags.put(DataCache.class, DATA_CACHE);
        _tags.put(DataStoreId.class, DATASTORE_ID);
        _tags.put(Dependent.class, DEPENDENT);
        _tags.put(DetachedState.class, DETACHED_STATE);
        _tags.put(ElementDependent.class, ELEM_DEPENDENT);
        _tags.put(ElementType.class, ELEM_TYPE);
        _tags.put(ExternalValues.class, EXTERNAL_VALS);
        _tags.put(Externalizer.class, EXTERNALIZER);
        _tags.put(Convert.class, CONVERT);
        _tags.put(Factory.class, FACTORY);
        _tags.put(FetchGroup.class, FETCH_GROUP);
        _tags.put(FetchGroups.class, FETCH_GROUPS);
        _tags.put(InverseLogical.class, INVERSE_LOGICAL);
        _tags.put(KeyDependent.class, KEY_DEPENDENT);
        _tags.put(KeyType.class, KEY_TYPE);
        _tags.put(LoadFetchGroup.class, LOAD_FETCH_GROUP);
        _tags.put(LRS.class, LRS);
        _tags.put(ManagedInterface.class, MANAGED_INTERFACE);
        _tags.put(ReadOnly.class, READ_ONLY);
        _tags.put(Type.class, TYPE);
    }

    private final OpenJPAConfiguration _conf;
    private final Log _log;
    private MetaDataRepository _repos = null;
    private ClassLoader _envLoader = null;
    private boolean _override = false;
    private int _mode = MODE_NONE;

    // packages and their parse modes
    private final Map<Package, Integer> _pkgs = new HashMap<>();

    // the class we were invoked to parse
    protected Class<?> _cls = null;
    protected Stack<Class<?>> _stack = new Stack<>();
    private File _file = null;

    /**
     * Constructor; supply configuration.
     */
    public AnnotationPersistenceMetaDataParser(OpenJPAConfiguration conf) {
        _conf = conf;
        _log = conf.getLog(OpenJPAConfiguration.LOG_METADATA);
    }

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

    /**
     * Metadata log.
     */
    public Log getLog() {
        return _log;
    }

    /**
     * Returns the repository for this parser. If none has been set,
     * create a new repository and sets it.
     */
    public MetaDataRepository getRepository() {
        if (_repos == null) {
            MetaDataRepository repos = _conf.newMetaDataRepositoryInstance();
            MetaDataFactory mdf = repos.getMetaDataFactory();
            if (mdf instanceof DelegatingMetaDataFactory)
                mdf = ((DelegatingMetaDataFactory) mdf).getInnermostDelegate();
            if (mdf instanceof PersistenceMetaDataFactory)
                ((PersistenceMetaDataFactory) mdf).setAnnotationParser(this);
            _repos = repos;
        }
        return _repos;
    }

    /**
     * Set the metadata repository for this parser.
     */
    public void setRepository(MetaDataRepository repos) {
        _repos = repos;
    }

    /**
     * Return the environmental class loader to pass on to parsed
     * metadata instances.
     */
    public ClassLoader getEnvClassLoader() {
        return _envLoader;
    }

    /**
     * Set the environmental class loader to pass on to parsed
     * metadata instances.
     */
    public void setEnvClassLoader(ClassLoader loader) {
        _envLoader = loader;
    }

    /**
     * Whether to allow later parses of mapping information to override
     * earlier information for the same class. Defaults to false. Useful
     * when a tool is mapping a class, so that annotation partial mapping
     * information can be used even when mappings are stored in another
     * location.
     */
    public boolean getMappingOverride() {
        return _override;
    }

    /**
     * Whether to allow later parses of mapping information to override
     * earlier information for the same class. Defaults to false. Useful
     * when a tool is mapping a class, so that annotation partial mapping
     * information can be used even when mappings are stored in another
     * location.
     */
    public void setMappingOverride(boolean override) {
        _override = override;
    }

    /**
     * The parse mode.
     */
    public int getMode() {
        return _mode;
    }

    /**
     * The parse mode.
     */
    public void setMode(int mode, boolean on) {
        if (mode == MODE_NONE)
            _mode = MODE_NONE;
        else if (on)
            _mode |= mode;
        else
            _mode &= ~mode;
    }

    /**
     * The parse mode.
     */
    public void setMode(int mode) {
        _mode = mode;
    }

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

    /**
     * Returns true if we're in mapping mode or in metadata mode with
     * mapping overide enabled.
     */
    protected boolean isMappingOverrideMode() {
        return isMappingMode() || (_override && isMetaDataMode());
    }

    /**
     * Clear caches.
     */
    public void clear() {
    	_stack.clear();
        _cls = null;
        _file = null;
        _pkgs.clear();
    }

    /**
     * Parse persistence metadata for the given class.
     */
    public void parse(Class<?> cls) {
        if (_log.isTraceEnabled())
            _log.trace(_loc.get("parse-class", cls.getName()));

        _cls = cls;
        _stack.push(cls);
        try {
            parsePackageAnnotations();
            ClassMetaData meta = parseClassAnnotations();
            updateSourceMode(meta);
        } finally {
        	_stack.pop();
            _cls = _stack.isEmpty() ? null : _stack.peek();
            _file = null;
        }
    }

    /**
     * Update the source mode to the class package and class to indicate that
     * we've fully parsed them.
     */
    private void updateSourceMode(ClassMetaData meta) {
        if (_cls.getPackage() != null)
            addSourceMode(_cls.getPackage(), _mode);
        if (meta != null)
            meta.setSourceMode(_mode, true);
    }

    /**
     * Parse information in package-level class annotations.
     */
    private void parsePackageAnnotations() {
        Package pkg = _cls.getPackage();
        if (pkg == null)
            return;

        int pkgMode = getSourceMode(pkg);
        if (pkgMode == 0 && _log.isTraceEnabled())
            _log.trace(_loc.get("parse-package", _cls.getName()));
        if ((pkgMode & _mode) == _mode) // already visited
            return;

        MetaDataTag tag;
        for (Annotation anno : pkg.getDeclaredAnnotations()) {
            tag = _tags.get(anno.annotationType());
            if (tag == null) {
                handleUnknownPackageAnnotation(pkg, anno);
                continue;
            }

            switch (tag) {
                case NATIVE_QUERIES:
                    if (isQueryMode() && (pkgMode & MODE_QUERY) == 0)
                        parseNamedNativeQueries(pkg,
                            ((NamedNativeQueries) anno).value());
                    break;
                case NATIVE_QUERY:
                    if (isQueryMode() && (pkgMode & MODE_QUERY) == 0)
                        parseNamedNativeQueries(pkg, (NamedNativeQuery) anno);
                    break;
                case QUERIES:
                    if (isQueryMode() && (pkgMode & MODE_QUERY) == 0)
                        parseNamedQueries(pkg, ((NamedQueries) anno).value());
                    break;
                case QUERY:
                    if (isQueryMode() && (pkgMode & MODE_QUERY) == 0)
                        parseNamedQueries(pkg, (NamedQuery) anno);
                    break;
                case STOREDPROCEDURE_QUERIES:
                    if (isQueryMode())
                        parseNamedStoredProcedureQueries(pkg, ((NamedStoredProcedureQueries) anno).value());
                    break;
                case STOREDPROCEDURE_QUERY:
                    if (isQueryMode())
                        parseNamedStoredProcedureQueries(pkg, ((NamedStoredProcedureQuery) anno));
                    break;
                case SEQ_GENERATOR:
                    if (isMappingOverrideMode() &&
                        (pkgMode & MODE_MAPPING) == 0)
                        parseSequenceGenerator(pkg, (SequenceGenerator) anno);
                    break;
                default:
                    throw new UnsupportedException(_loc.get("unsupported", pkg,
                        anno.toString()));
            }
        }

        // always parse mapping stuff after metadata stuff, in case there are
        // dependencies on metadata
        if (isMappingOverrideMode() && (pkgMode & MODE_MAPPING) == 0)
            parsePackageMappingAnnotations(pkg);
    }

    /**
     * Parse package mapping annotations.
     */
    protected void parsePackageMappingAnnotations(Package pkg) {
    }

    /**
     * Allow subclasses to handle unknown annotations.
     */
    protected boolean handleUnknownPackageAnnotation(Package pkg,
        Annotation anno) {
        return false;
    }

    /**
     * The source mode for the given package.
     */
    private int getSourceMode(Package pkg) {
        Number num = _pkgs.get(pkg);
        return (num == null) ? 0 : num.intValue();
    }

    /**
     * Add to the source mode for the given package.
     */
    private void addSourceMode(Package pkg, int mode) {
        Integer num = _pkgs.get(pkg);
        if (num == null)
            num = mode;
        else
            num = num | mode;
        _pkgs.put(pkg, num);
    }

    /**
     * Read annotations for the current type.
     */
    private ClassMetaData parseClassAnnotations() {
        // Check to see if there is cached metadata for the class that we are currently parsing. It
        // is possible that one of the annotations (Entity, Embeddable, MappedSuperclass) is in the
        // orm.xml. We still need to look at these files for other annotations and more importantly
        // setup defaults (ie: Basic fields).
        ClassMetaData m = getRepository().getCachedMetaData(_cls);
        if (m == null) {
            if (!AccessController.doPrivileged(J2DoPrivHelper.isAnnotationPresentAction(_cls, Entity.class))
                && !AccessController.doPrivileged(J2DoPrivHelper.isAnnotationPresentAction(_cls, Embeddable.class))
                && !AccessController.doPrivileged(J2DoPrivHelper.isAnnotationPresentAction(_cls,
                    MappedSuperclass.class)))
                return null;
        }
        // find / create metadata
        ClassMetaData meta = (m == null) ? getMetaData() : m;
        if (meta == null)
            return null;

        Entity entity = _cls.getAnnotation(Entity.class);
        MappedSuperclass mapped = _cls.getAnnotation(MappedSuperclass.class);
        Embeddable embeddable = _cls.getAnnotation(Embeddable.class);
        if (isMetaDataMode()) {
            meta.setAbstract(mapped != null);
            if (embeddable != null) meta.setEmbeddable();
            // while the spec only provides for embedded exclusive, it doesn't
            // seem hard to support otherwise
            if (entity == null)
                meta.setEmbeddedOnly(true);
            else {
                meta.setEmbeddedOnly(false);
                if (!StringUtil.isEmpty(entity.name()))
                    meta.setTypeAlias(entity.name());
            }
        }

        // track fetch groups to parse them after fields, since they
        // rely on field metadata
        FetchGroup[] fgs = null;
        DetachedState detached = null;

        // track listeners since we need to merge them with entity callbacks
        Collection<LifecycleCallbacks>[] listeners = null;
        MetaDataTag tag;
        for (Annotation anno : _cls.getDeclaredAnnotations()) {
            tag = _tags.get(anno.annotationType());
            if (tag == null) {
                handleUnknownClassAnnotation(meta, anno);
                continue;
            }

            switch (tag) {
                case ENTITY_LISTENERS:
                    if (isMetaDataMode())
                        listeners = parseEntityListeners(meta,
                            (EntityListeners) anno);
                    break;
                case EXCLUDE_DEFAULT_LISTENERS:
                    if (isMetaDataMode())
                        meta.getLifecycleMetaData()
                            .setIgnoreSystemListeners(true);
                    break;
                case EXCLUDE_SUPERCLASS_LISTENERS:
                    if (isMetaDataMode())
                        meta.getLifecycleMetaData().setIgnoreSuperclassCallbacks
                            (LifecycleMetaData.IGNORE_HIGH);
                    break;
                case FLUSH_MODE:
                    if (isMetaDataMode())
                        warnFlushMode(meta);
                    break;
                case ID_CLASS:
                    if (isMetaDataMode()) {
                    	Class<?> idClass = ((IdClass)anno).value();
                    	if (!Serializable.class.isAssignableFrom(idClass)) {
                    		_log.warn(_loc.get("id-class-not-serializable", idClass, _cls).toString());
                    	}
                        meta.setObjectIdType(((IdClass) anno).value(), true);
                    }
                    break;
                case NATIVE_QUERIES:
                    if (isQueryMode() && (meta.getSourceMode() & MODE_QUERY)==0)
                        parseNamedNativeQueries(_cls,
                            ((NamedNativeQueries) anno).value());
                    break;
                case NATIVE_QUERY:
                    if (isQueryMode() && (meta.getSourceMode() & MODE_QUERY)==0)
                        parseNamedNativeQueries(_cls, (NamedNativeQuery) anno);
                    break;
                case QUERIES:
                    if (isQueryMode() && (meta.getSourceMode() & MODE_QUERY)==0)
                        parseNamedQueries(_cls, ((NamedQueries) anno).value());
                    break;
                case QUERY:
                    if (isQueryMode() && (meta.getSourceMode() & MODE_QUERY)==0)
                        parseNamedQueries(_cls, (NamedQuery) anno);
                    break;
                case STOREDPROCEDURE_QUERIES:
                    if (isQueryMode())
                        parseNamedStoredProcedureQueries(_cls, ((NamedStoredProcedureQueries) anno).value());
                    break;
                case STOREDPROCEDURE_QUERY:
                    if (isQueryMode())
                        parseNamedStoredProcedureQueries(_cls, ((NamedStoredProcedureQuery) anno));
                    break;
                case SEQ_GENERATOR:
                    if (isMappingOverrideMode())
                        parseSequenceGenerator(_cls, (SequenceGenerator) anno);
                    break;
                case DATA_CACHE:
                    if (isMetaDataMode())
                        parseDataCache(meta, (DataCache) anno);
                    break;
                case DATASTORE_ID:
                    if (isMetaDataMode())
                        parseDataStoreId(meta, (DataStoreId) anno);
                    break;
                case DETACHED_STATE:
                    detached = (DetachedState) anno;
                    break;
                case FETCH_GROUP:
                    if (isMetaDataMode())
                        fgs = new FetchGroup[]{ (FetchGroup) anno };
                    break;
                case FETCH_GROUPS:
                    if (isMetaDataMode())
                        fgs = ((FetchGroups) anno).value();
                    break;
                case MANAGED_INTERFACE:
                    if (isMetaDataMode())
                        parseManagedInterface(meta, (ManagedInterface) anno);
                    break;
                case ACCESS:
                    if (isMetaDataMode())
                        parseAccess(meta, (Access)anno);
                    break;
                case CACHEABLE:
                    if (isMetaDataMode()) {
                        parseCache(meta, (Cacheable) anno);
                    }
                    break;
                default:
                    throw new UnsupportedException(_loc.get("unsupported", _cls,
                        anno.toString()));
            }
        }

        if (isMetaDataMode()) {
            parseDetachedState(meta, detached);

            // merge callback methods with declared listeners
            int[] highs = null;
            if (listeners != null) {
                highs = new int[listeners.length];
                for (int i = 0; i < listeners.length; i++)
                    if (listeners[i] != null)
                        highs[i] = listeners[i].size();
            }
            recordCallbacks(meta, parseCallbackMethods(_cls, listeners, false,
                false, getRepository()), highs, false);

            // scan possibly non-PC hierarchy for callbacks.
            // redundant for PC superclass but we don't know that yet
            // so let LifecycleMetaData determine that
            if (_cls.getSuperclass() != null &&
                !Object.class.equals(_cls.getSuperclass())) {
                recordCallbacks(meta, parseCallbackMethods(_cls.getSuperclass(),
                    null, true, false, getRepository()), null, true);
            }
        }

        for (FieldMetaData fmd : meta.getDeclaredFields())
            if (fmd.getManagement() == FieldMetaData.MANAGE_PERSISTENT)
                parseMemberAnnotations(fmd);
        // parse fetch groups after fields
        if (fgs != null)
            parseFetchGroups(meta, fgs);

        // always parse mapping after metadata in case there are dependencies
        if (isMappingOverrideMode()) {
            parseClassMappingAnnotations(meta);
            for (FieldMetaData fmd : meta.getDeclaredFields())
                if (fmd.getManagement() == FieldMetaData.MANAGE_PERSISTENT)
                    parseMemberMappingAnnotations(fmd);
        }
        return meta;
    }

    /**
     * Set the explicit access type, if specified.
     */
    private void parseAccess(ClassMetaData meta, Access access) {
    	if (access != null) {
    		meta.setAccessType(AccessCode.EXPLICIT
            | (access.value() == AccessType.FIELD ?
            	AccessCode.FIELD : AccessCode.PROPERTY));
    	}
    }

    /**
     * Parse class mapping annotations.
     */
    protected void parseClassMappingAnnotations(ClassMetaData meta) {
    }

    /**
     * Allow subclasses to handle unknown annotations.
     */
    protected boolean handleUnknownClassAnnotation(ClassMetaData meta,
        Annotation anno) {
        return false;
    }

    /**
     * Find or create metadata for the given type. May return null if
     * this class has already been parsed fully.
     */
    private ClassMetaData getMetaData() {
        ClassMetaData meta = getRepository().getCachedMetaData(_cls);
        if (meta != null
            && ((isMetaDataMode()
                && (meta.getSourceMode() & MODE_META) != 0)
                || (isMappingMode() && (meta.getSourceMode() & MODE_MAPPING) != 0) ) ) {
            if (_log.isWarnEnabled()) {
                _log.warn(_loc.get("dup-metadata", _cls.getName()));
            }
            if(_log.isTraceEnabled()) {
                _log.trace(String.format(
                    "MetaData originally obtained from file: %s under mode :%d with scope %s, and type :%d",
                    meta.getSourceName(), meta.getSourceMode(), meta.getSourceScope(), meta.getSourceType()));
            }
            return null;
        }

        if (meta == null) {
            meta = getRepository().addMetaData(_cls, getAccessCode(_cls));
            meta.setEnvClassLoader(_envLoader);
            meta.setSourceMode(MODE_NONE);
            meta.setSource(getSourceFile(), SourceTracker.SRC_ANNOTATIONS, getSourceFile() == null ? ""
                : getSourceFile().getPath());
        }
        return meta;
    }

    /**
     * Gets the explicit access for the class, if any.
     * Explicit access type specification does not affect the access type of
     * other entity classes or mapped super classes in the entity hierarchy.
     */
    private int getAccessCode(Class<?> cls) {
        int accessCode = AccessCode.UNKNOWN;
        Access access = AccessController.doPrivileged(
            J2DoPrivHelper.getAnnotationAction(cls, Access.class));
        if (access != null) {
            accessCode |=  AccessCode.EXPLICIT |
                (access.value() == AccessType.FIELD ?
                AccessCode.FIELD : AccessCode.PROPERTY);
        }
        return accessCode;
    }

    /**
     * Determine the source file we're parsing.
     */
    protected File getSourceFile() {
        if (_file != null)
            return _file;

        Class<?> cls = _cls;
        while (cls.getEnclosingClass() != null)
            cls = cls.getEnclosingClass();

        String rsrc = StringUtil.replace(cls.getName(), ".", "/");
        ClassLoader loader = AccessController.doPrivileged(
            J2DoPrivHelper.getClassLoaderAction(cls));
        if (loader == null)
            loader = AccessController.doPrivileged(
                J2DoPrivHelper.getSystemClassLoaderAction());
        if (loader == null)
            return null;
        URL url = AccessController.doPrivileged(
            J2DoPrivHelper.getResourceAction(loader, rsrc + ".java"));
        if (url == null) {
            url = AccessController.doPrivileged(
                J2DoPrivHelper.getResourceAction(loader, rsrc + ".class"));
            if (url == null)
                return null;
        }
        try {
            _file = new File(url.toURI());
        } catch (URISyntaxException e) {
        } catch (IllegalArgumentException iae) {
            // this is thrown when the URI is non-hierarchical (aka JBoss)
        }
        return _file;
    }

    /**
     * Parse @DataStoreId.
     */
    private void parseDataStoreId(ClassMetaData meta, DataStoreId id) {
        parseDataStoreId(meta, id.strategy(), id.generator());
    }

    static void parseDataStoreId(ClassMetaData meta, GenerationType strategy,
        String generator) {
        meta.setIdentityType(ClassMetaData.ID_DATASTORE);

        int strat = getGeneratedValueStrategy(meta, strategy,
            generator);
        if (strat != -1)
            meta.setIdentityStrategy(strat);
        else {
            switch (strategy) {
                case TABLE:
                case SEQUENCE:
                    // technically we should have separate system table and
                    // sequence generators, but it's easier to just rely on
                    // the system org.apache.openjpa.Sequence setting for both
                    if (StringUtil.isEmpty(generator))
                        meta.setIdentitySequenceName(
                            SequenceMetaData.NAME_SYSTEM);
                    else
                        meta.setIdentitySequenceName(generator);
                    break;
                case AUTO:
                    meta.setIdentityStrategy(ValueStrategies.NATIVE);
                    break;
                case IDENTITY:
                    meta.setIdentityStrategy(ValueStrategies.AUTOASSIGN);
                    break;
                default:
                    throw new UnsupportedException(strategy.toString());
            }
        }
    }

    /**
     * Warn that @FlushMode is not supported.
     */
    private void warnFlushMode(Object context) {
        if (_log.isWarnEnabled())
            _log.warn(_loc.get("unsupported", "FlushMode", context));
    }

    /**
     * Parse @DataCache.
     *
     */
    private void parseDataCache(ClassMetaData meta, DataCache cache) {
        parseDataCache(meta, cache.enabled(), cache.name(), cache.timeout());
    }

    static void parseDataCache(ClassMetaData meta,
            boolean enabled,
            String name,
            int timeout) {
        if (timeout != Integer.MIN_VALUE) {
            meta.setDataCacheTimeout(timeout);
        }
        String cacheName = name;
        if (StringUtil.isEmpty(cacheName)) {
            cacheName = org.apache.openjpa.datacache.DataCache.NAME_DEFAULT;
        }
        meta.setDataCacheName(enabled ? cacheName : null);
    }

    private void parseManagedInterface(ClassMetaData meta,
        ManagedInterface iface) {
        meta.setManagedInterface(true);
    }

    /**
     * Parse @DetachedState. The annotation may be null.
     */
    private void parseDetachedState(ClassMetaData meta,
        DetachedState detached) {
        if (detached != null) {
            if (!detached.enabled())
                meta.setDetachedState(null);
            else if (StringUtil.isEmpty(detached.fieldName()))
                meta.setDetachedState(ClassMetaData.SYNTHETIC);
            else
                meta.setDetachedState(detached.fieldName());
        } else {
            Field[] fields = (Field[]) AccessController.doPrivileged(
                J2DoPrivHelper.getDeclaredFieldsAction(
                    meta.getDescribedType()));
            for (Field field : fields)
                if (AccessController.doPrivileged(J2DoPrivHelper
                        .isAnnotationPresentAction(field, DetachedState.class)))
                    meta.setDetachedState(field.getName());
        }
    }

    /**
     * Parse @EntityListeners
     */
    private Collection<LifecycleCallbacks>[] parseEntityListeners
        (ClassMetaData meta, EntityListeners listeners) {
        Class<?>[] classes = listeners.value();
        Collection<Class<?>> listenerColl = null;
        Collection<LifecycleCallbacks>[] parsed = null;
        for (Class<?> cls : classes) {
            if (!_conf.getCallbackOptionsInstance().getAllowsDuplicateListener()) {
                if (listenerColl == null)
                    listenerColl = new ArrayList<>();
                if (listenerColl.contains(cls))
                    continue;
                listenerColl.add(cls);
            }

            parsed = parseCallbackMethods(cls, parsed, true, true,
                getRepository());
        }
        return parsed;
    }

    /**
     * Parse callback methods into the given array, and return that array,
     * creating one if null. Each index into the array is a collection of
     * callback adapters for that numeric event type.
     *
     * @param sups whether to scan superclasses
     * @param listener whether this is a listener or not
     */
    public static Collection<LifecycleCallbacks>[] parseCallbackMethods
        (Class<?> cls, Collection<LifecycleCallbacks>[] callbacks, boolean sups,
        boolean listener, MetaDataRepository repos) {

        if (cls == null)
            throw new IllegalArgumentException("cls cannot be null");

        // first sort / filter based on inheritance
        Set<Method> methods = new TreeSet<>(MethodComparator.
            getInstance());

        int mods;
        Class<?> sup = cls;
        MethodKey key;
        Set<MethodKey> seen = new HashSet<>();
        do {
            for (Method m : (Method[]) AccessController.doPrivileged(J2DoPrivHelper.getDeclaredMethodsAction(sup))) {
                mods = m.getModifiers();
                if (Modifier.isStatic(mods) || Modifier.isFinal(mods) ||
                    Object.class.equals(m.getDeclaringClass()))
                    continue;

                key = new MethodKey(m);
                if (!seen.contains(key)) {
                    methods.add(m);
                    seen.add(key);
                }
            }
            sup = sup.getSuperclass();
        } while (sups && !Object.class.equals(sup));

        OpenJPAConfiguration conf = repos.getConfiguration();
        for (Method m : methods) {
            for (Annotation anno : (Annotation[]) AccessController
                .doPrivileged(J2DoPrivHelper
                    .getDeclaredAnnotationsAction(m))) {
                MetaDataTag tag = _tags.get(anno.annotationType());
                if (tag == null)
                    continue;
                int[] events = MetaDataParsers.getEventTypes(tag, conf);
                if (events == null)
                    continue;

                if (callbacks == null)
                    callbacks = (Collection<LifecycleCallbacks>[])
                        new Collection[LifecycleEvent.ALL_EVENTS.length];

                for (int e : events) {
                    if (callbacks[e] == null)
                        callbacks[e] = new ArrayList<>(3);
                    MetaDataParsers.validateMethodsForSameCallback(cls,
                            callbacks[e], m, tag, conf, repos.getLog());
                    if (listener) {
                        callbacks[e].add(new BeanLifecycleCallbacks(cls, m,
                                false));
                    }
                    else {
                        callbacks[e].add(new MethodLifecycleCallbacks(m,
                                false));
                    }
                }
            }
        }
        return callbacks;
    }

    /**
     * Store lifecycle metadata.
     */
    private void recordCallbacks(ClassMetaData cls,
        Collection<LifecycleCallbacks>[] callbacks, int[] highs,
        boolean superClass) {
        if (callbacks == null)
            return;
        LifecycleMetaData meta = cls.getLifecycleMetaData();
        LifecycleCallbacks[] array;
        for (int event : LifecycleEvent.ALL_EVENTS) {
            if (callbacks[event] == null)
                continue;
            array = callbacks[event].toArray
                (new LifecycleCallbacks[callbacks[event].size()]);

            if (superClass) {
                meta.setNonPCSuperclassCallbacks(event, array,
                    (highs == null) ? 0 : highs[event]);
            } else {
                meta.setDeclaredCallbacks(event, array,
                    (highs == null) ? 0 : highs[event]);
            }
        }
    }

    /**
     * Create fetch groups.
     * If FetchGroup A includes FetchGroup B, then a bi-link is set between
     * A and B. Both A and B must be declared in the same Class.
     * <br>
     * Call {@link #parseFetchAttribute(ClassMetaData, org.apache.openjpa.meta.FetchGroup, FetchAttributeImpl)}
     * only after the
     * bi-links have been established, because a field f will not only add the
     * fetch group A which explicitly includes f to its custom fetch groups but
     * also will also add any fetch group B that includes A.
     */
    static void parseFetchGroups(ClassMetaData meta, FetchGroup... groups) {
       FetchGroupImpl[] fetchGroupImpls = new FetchGroupImpl[groups.length];
       for (int i = 0; i < groups.length; i++) {
           FetchAttribute[] fetchAttributes = groups[i].attributes();
           FetchAttributeImpl[] fetchAttributeImpls= null;
           if (fetchAttributes != null && fetchAttributes.length > 0) {
               fetchAttributeImpls = new FetchAttributeImpl[fetchAttributes.length];
               for (int j = 0; j < fetchAttributes.length; j++) {
                   fetchAttributeImpls[j] =
                           new FetchAttributeImpl(fetchAttributes[j].name(), fetchAttributes[j].recursionDepth());
               }
           }
           fetchGroupImpls[i] = new FetchGroupImpl(groups[i].name(), groups[i].postLoad());
           if (fetchAttributeImpls != null) {
               fetchGroupImpls[i].setAttributes(fetchAttributeImpls);
           }
           if (groups[i].fetchGroups() != null) {
               fetchGroupImpls[i].setFetchGroups(groups[i].fetchGroups());
           }
       }

       parseFetchGroups(meta, fetchGroupImpls);
    }

   /**
    * Parse fetch group input for the FetchGroup and FetchGroups annotations
    * as well as for the fetch-group and fetch-groups XML metadata
    * @param meta
    * @param groups
    */
   static void parseFetchGroups(ClassMetaData meta, FetchGroupImpl... groups) {
       org.apache.openjpa.meta.FetchGroup fg;
       for (FetchGroupImpl group : groups) {
           if (StringUtil.isEmpty(group.name()))
               throw new MetaDataException(_loc.get("unnamed-fg", meta));

           fg = meta.addDeclaredFetchGroup(group.name());
           if (group.postLoad())
               fg.setPostLoad(true);
           for (String s : group.fetchGroups()) {
               fg.addDeclaredInclude(s);
           }
       }
       // Add the parent-child style bi-links between fetch groups in a
       // separate pass.
       for (FetchGroupImpl group:groups) {
           fg = meta.getFetchGroup(group.name());
           String[] includedFetchGropNames = fg.getDeclaredIncludes();
           for (String includedFectchGroupName:includedFetchGropNames) {
               org.apache.openjpa.meta.FetchGroup child =
               meta.getFetchGroup(includedFectchGroupName);
               if (child == null)
                   throw new UserException(_loc.get("missing-included-fg",
                           meta.getDescribedType().getName(), fg.getName(),
                           includedFectchGroupName));
               child.addContainedBy(fg);
           }
       }

       for (FetchGroupImpl group : groups) {
           fg = meta.getFetchGroup(group.name());
           for (FetchAttributeImpl attr : group.attributes())
               parseFetchAttribute(meta, fg, attr);
       }
   }


    /**
     * Set a field's fetch group.
     */
    private static void parseFetchAttribute(ClassMetaData meta,
        org.apache.openjpa.meta.FetchGroup fg, FetchAttributeImpl attr) {
        FieldMetaData field = meta.getDeclaredField(attr.name());
        if (field == null
            || field.getManagement() != FieldMetaData.MANAGE_PERSISTENT)
            throw new MetaDataException(_loc.get("bad-fg-field", fg.getName(),
                meta, attr.name()));

        field.setInFetchGroup(fg.getName(), true);
        Set<String> parentFetchGroups = fg.getContainedBy();
        for (Object parentFetchGroup : parentFetchGroups)
        	field.setInFetchGroup(parentFetchGroup.toString(), true);
        if (attr.recursionDepth() != Integer.MIN_VALUE)
            fg.setRecursionDepth(field, attr.recursionDepth());
    }

    /**
     * Read annotations for the given member.
     */
    private void parseMemberAnnotations(FieldMetaData fmd) {
        // look for persistence strategy in annotation table
        Member member = getRepository().getMetaDataFactory().getDefaults().
            getBackingMember(fmd);
        PersistenceStrategy pstrat = PersistenceMetaDataDefaults.
            getPersistenceStrategy(fmd, member);
        if (pstrat == null)
            return;
        fmd.setExplicit(true);

        AnnotatedElement el = (AnnotatedElement) member;
        boolean lob = AccessController.doPrivileged(J2DoPrivHelper
                .isAnnotationPresentAction(el, Lob.class));
        if (isMetaDataMode()) {
            switch (pstrat) {
                case BASIC:
                    parseBasic(fmd, (Basic) el.getAnnotation(Basic.class), lob);
                    break;
                case MANY_ONE:
                    parseManyToOne(fmd, (ManyToOne) el.getAnnotation
                        (ManyToOne.class));
                    break;
                case ONE_ONE:
                    parseOneToOne(fmd, (OneToOne) el.getAnnotation
                        (OneToOne.class));
                    break;
                case EMBEDDED:
                    parseEmbedded(fmd, (Embedded) el.getAnnotation
                        (Embedded.class));
                    break;
                case ONE_MANY:
                    parseOneToMany(fmd, (OneToMany) el.getAnnotation
                        (OneToMany.class));
                    break;
                case MANY_MANY:
                    parseManyToMany(fmd, (ManyToMany) el.getAnnotation
                        (ManyToMany.class));
                    break;
                case PERS:
                    parsePersistent(fmd, (Persistent) el.getAnnotation
                        (Persistent.class));
                    break;
                case PERS_COLL:
                    parsePersistentCollection(fmd, (PersistentCollection)
                        el.getAnnotation(PersistentCollection.class));
                    break;
                case ELEM_COLL:
                    parseElementCollection(fmd, (ElementCollection)
                        el.getAnnotation(ElementCollection.class));
                    break;
                case PERS_MAP:
                    parsePersistentMap(fmd, (PersistentMap)
                        el.getAnnotation(PersistentMap.class));
                    break;
                case TRANSIENT:
                    break;
                default:
                    throw new InternalException();
            }
        }

        if (isMappingOverrideMode() && lob)
            parseLobMapping(fmd);

        // extensions
        MetaDataTag tag;
        for (Annotation anno : el.getDeclaredAnnotations()) {
            tag = _tags.get(anno.annotationType());
            if (tag == null) {
                handleUnknownMemberAnnotation(fmd, anno);
                continue;
            }

            switch (tag) {
                case ACCESS:
                    parseAccess(fmd, (Access)anno);
                    break;
                case FLUSH_MODE:
                    if (isMetaDataMode())
                        warnFlushMode(fmd);
                    break;
                case GENERATED_VALUE:
                    if (isMappingOverrideMode())
                        parseGeneratedValue(fmd, (GeneratedValue) anno);
                    break;
                case ID:
                case EMBEDDED_ID:
                    fmd.setPrimaryKey(true);
                    break;
                case MAPPED_BY_ID:
                    parseMapsId(fmd, (MapsId)anno);
                    break;
                case MAP_KEY:
                    if (isMappingOverrideMode())
                        parseMapKey(fmd, (MapKey) anno);
                    break;
                case MAP_KEY_CLASS:
                    if (isMappingOverrideMode())
                        parseMapKeyClass(fmd, (MapKeyClass) anno);
                    break;
                case ORDER_BY:
                    parseOrderBy(fmd,
                        (OrderBy) el.getAnnotation(OrderBy.class));
                    break;
                case SEQ_GENERATOR:
                    if (isMappingOverrideMode())
                        parseSequenceGenerator(el, (SequenceGenerator) anno);
                    break;
                case VERSION:
                    fmd.setVersion(true);
                    break;
                case DEPENDENT:
                    if (isMetaDataMode() && ((Dependent) anno).value())
                        fmd.setCascadeDelete(ValueMetaData.CASCADE_AUTO);
                    break;
                case ELEM_DEPENDENT:
                    if (isMetaDataMode() && ((ElementDependent) anno).value())
                        fmd.getElement().setCascadeDelete
                            (ValueMetaData.CASCADE_AUTO);
                    break;
                case ELEM_TYPE:
                    if (isMetaDataMode())
                        fmd.getElement().setTypeOverride(toOverrideType
                            (((ElementType) anno).value()));
                    break;
                case EXTERNAL_VALS:
                    if (isMetaDataMode())
                        fmd.setExternalValues(StringUtil.join(((ExternalValues) anno).value(), ","));
                    break;
                case EXTERNALIZER:
                    if (isMetaDataMode())
                        fmd.setExternalizer(((Externalizer) anno).value());
                    break;
                case FACTORY:
                    if (isMetaDataMode())
                        fmd.setFactory(((Factory) anno).value());
                    break;
                case INVERSE_LOGICAL:
                    if (isMetaDataMode())
                        fmd.setInverse(((InverseLogical) anno).value());
                    break;
                case KEY_DEPENDENT:
                    if (isMetaDataMode() && ((KeyDependent) anno).value())
                        fmd.getKey()
                            .setCascadeDelete(ValueMetaData.CASCADE_AUTO);
                    break;
                case KEY_TYPE:
                    if (isMetaDataMode())
                        fmd.getKey().setTypeOverride(toOverrideType(((KeyType)
                            anno).value()));
                    break;
                case LOAD_FETCH_GROUP:
                	if (isMetaDataMode())
                        fmd.setLoadFetchGroup(((LoadFetchGroup) anno).value());
                	break;
                case LRS:
                    if (isMetaDataMode())
                        fmd.setLRS(((LRS) anno).value());
                    break;
                case READ_ONLY:
                    if (isMetaDataMode())
                        parseReadOnly(fmd, (ReadOnly) anno);
                    break;
                case TYPE:
                    if (isMetaDataMode())
                        fmd.setTypeOverride(toOverrideType(((Type) anno).
                            value()));
                    break;
                case CONVERT:
                    if (isMetaDataMode() && !((Convert) anno).disableConversion())
                        fmd.setConverter(((Convert) anno).converter());
                    break;
                default:
                    throw new UnsupportedException(_loc.get("unsupported", fmd,
                        anno.toString()));
            }
        }
    }

    /**
     * Parse member mapping components.
     */
    protected void parseMemberMappingAnnotations(FieldMetaData fmd) {
    }

    /**
     * Parse @Cache.
     */
    private void parseCache(ClassMetaData meta, Cacheable cacheable) {
        meta.setCacheEnabled(cacheable.value());
    }

    /**
     * Allow subclasses to handle unknown annotations.
     */
    protected boolean handleUnknownMemberAnnotation(FieldMetaData fmd,
        Annotation anno) {
        return false;
    }

    /**
     * Convert the given class to its OpenJPA type override equivalent.
     */
    public static Class<?> toOverrideType(Class<?> cls) {
        return (cls == Entity.class)
            ? org.apache.openjpa.enhance.PersistenceCapable.class : cls;
    }

    /**
     * Parse @ReadOnly.
     */
    private void parseReadOnly(FieldMetaData fmd, ReadOnly ro) {
        if (ro.value() == UpdateAction.RESTRICT)
            fmd.setUpdateStrategy(UpdateStrategies.RESTRICT);
        else if (ro.value() == UpdateAction.IGNORE)
            fmd.setUpdateStrategy(UpdateStrategies.IGNORE);
        else
            throw new InternalException();
    }

    /**
     * Sets value generation information for the given field.
     */
    private void parseGeneratedValue(FieldMetaData fmd, GeneratedValue gen) {
        GenerationType strategy = gen.strategy();
        String generator = gen.generator();
        parseGeneratedValue(fmd, strategy, generator);
    }

    /**
     * Sets value generation information for the given field.
     */
    static void parseGeneratedValue(FieldMetaData fmd, GenerationType strategy,
        String generator) {
        int strat = getGeneratedValueStrategy(fmd, strategy, generator);
        if (strat != -1)
            fmd.setValueStrategy(strat);
        else {
            switch (strategy) {
                case TABLE:
                case SEQUENCE:
                    // technically we should have separate system table and
                    // sequence generators, but it's easier to just rely on
                    // the system org.apache.openjpa.Sequence setting for both
                    if (StringUtil.isEmpty(generator))
                        fmd.setValueSequenceName(SequenceMetaData.NAME_SYSTEM);
                    else
                        fmd.setValueSequenceName(generator);
                    break;
                case IDENTITY:
                    fmd.setValueStrategy(ValueStrategies.AUTOASSIGN);
                    break;
                case AUTO:
                    if (fmd.getType() != UUID.class) {
                        fmd.setValueSequenceName(SequenceMetaData.NAME_SYSTEM);
                        return;
                    }
                case UUID:
                    if (fmd.getType() == UUID.class) {
                        fmd.setValueStrategy(ValueStrategies.UUID_JPA);
                        return;
                    } else if (fmd.getType() == String.class) {
                        fmd.setValueStrategy(ValueStrategies.UUID_TYPE4_CANON);
                        return;
                    }
                default:
                    throw new UnsupportedException(strategy.toString());
            }
        }
    }

    /**
     * Return the value strategy for the given generator, or -1 if the
     * strategy depends on the <code>GenerationType</code> rather than the
     * generator name.
     */
    private static int getGeneratedValueStrategy(Object context,
        GenerationType strategy, String generator) {
        if (strategy != AUTO || StringUtil.isEmpty(generator))
            return -1;

        if (Generator.UUID_HEX.equals(generator))
            return ValueStrategies.UUID_HEX;
        if (Generator.UUID_STRING.equals(generator))
            return ValueStrategies.UUID_STRING;
        if (Generator.UUID_TYPE4_HEX.equals(generator))
            return ValueStrategies.UUID_TYPE4_HEX;
        if (Generator.UUID_TYPE4_STRING.equals(generator))
            return ValueStrategies.UUID_TYPE4_STRING;
        throw new MetaDataException(_loc.get("generator-bad-strategy",
            context, generator));
    }

    /**
     * Parse @Basic. Given annotation may be null.
     */
    private void parseBasic(FieldMetaData fmd, Basic anno, boolean lob) {
        Class<?> type = fmd.getDeclaredType();
        if (lob && type != String.class
            && type != char[].class && type != Character[].class
            && type != byte[].class && type != Byte[].class)
            fmd.setSerialized(true);
        else if (!lob) {
            switch (fmd.getDeclaredTypeCode()) {
                case JavaTypes.OBJECT:
                    if (Enum.class.isAssignableFrom(type))
                        break;
                    // else no break
                case JavaTypes.COLLECTION:
                case JavaTypes.MAP:
                case JavaTypes.PC:
                case JavaTypes.PC_UNTYPED:
                    if (Serializable.class.isAssignableFrom(type))
                        fmd.setSerialized(true);
                    else
                        throw new MetaDataException(_loc.get("bad-meta-anno",
                            fmd, "Basic"));
                    break;
                case JavaTypes.ARRAY:
                    if (type == char[].class || type == Character[].class
                        || type == byte[].class || type == Byte[].class)
                        break;
                    if (Serializable.class.isAssignableFrom
                        (type.getComponentType()))
                        fmd.setSerialized(true);
                    else
                        throw new MetaDataException(_loc.get("bad-meta-anno",
                            fmd, "Basic"));
                    break;
            }
        }

        if (anno == null)
            return;
        fmd.setInDefaultFetchGroup(anno.fetch() == FetchType.EAGER);
        if (!anno.optional())
            fmd.setNullValue(FieldMetaData.NULL_EXCEPTION);
    }

    /**
     * Parse @ManyToOne.
     */
    private void parseManyToOne(FieldMetaData fmd, ManyToOne anno) {
        if (!JavaTypes.maybePC(fmd.getValue()))
            throw new MetaDataException(_loc.get("bad-meta-anno", fmd,
                "ManyToOne"));

        // don't specifically exclude relation from DFG b/c that will prevent
        // us from even reading the fk when reading from the primary table,
        // which is not what most users will want
        if (anno.fetch() == FetchType.EAGER)
            fmd.setInDefaultFetchGroup(true);
        if (!anno.optional())
            fmd.setNullValue(FieldMetaData.NULL_EXCEPTION);
        if (anno.targetEntity() != void.class)
            fmd.setTypeOverride(anno.targetEntity());
        setCascades(fmd, anno.cascade());
        fmd.setAssociationType(FieldMetaData.MANY_TO_ONE);
    }

    /**
     * Parse @OneToOne.
     */
    private void parseOneToOne(FieldMetaData fmd, OneToOne anno) {
        if (!JavaTypes.maybePC(fmd.getValue()))
            throw new MetaDataException(_loc.get("bad-meta-anno", fmd,
                "OneToOne"));

        // don't specifically exclude relation from DFG b/c that will prevent
        // us from even reading the fk when reading from the primary table,
        // which is not what most users will want
        if (anno.fetch() == FetchType.EAGER)
            fmd.setInDefaultFetchGroup(true);
        if (!anno.optional())
            fmd.setNullValue(FieldMetaData.NULL_EXCEPTION);

        if (isMappingOverrideMode() && !StringUtil.isEmpty(anno.mappedBy()))
            fmd.setMappedBy(anno.mappedBy());
        if (anno.targetEntity() != void.class)
            fmd.setTypeOverride(anno.targetEntity());
        setCascades(fmd, anno.cascade());
        setOrphanRemoval(fmd, anno.orphanRemoval());
        fmd.setAssociationType(FieldMetaData.ONE_TO_ONE);
    }

    /**
     * Parse @Embedded. Given annotation may be null.
     */
    private void parseEmbedded(FieldMetaData fmd, Embedded anno) {
        if (!JavaTypes.maybePC(fmd.getValue()))
            throw new MetaDataException(_loc.get("bad-meta-anno", fmd,
                "Embedded"));

        fmd.setInDefaultFetchGroup(true);
        fmd.setEmbedded(true);

        if (fmd.getEmbeddedMetaData() == null)
            fmd.addEmbeddedMetaData(getAccessCode(fmd.getDeclaredType()));
    }

    /**
     * Parse @OneToMany.
     */
    private void parseOneToMany(FieldMetaData fmd, OneToMany anno) {
        switch (fmd.getDeclaredTypeCode()) {
            case JavaTypes.ARRAY:
            case JavaTypes.COLLECTION:
            case JavaTypes.MAP:
                if (JavaTypes.maybePC(fmd.getElement()))
                    break;
                // no break
            default:
                throw new MetaDataException(_loc.get("bad-meta-anno", fmd,
                    "OneToMany"));
        }

        fmd.setInDefaultFetchGroup(anno.fetch() == FetchType.EAGER);
        if (isMappingOverrideMode() && !StringUtil.isEmpty(anno.mappedBy()))
            fmd.setMappedBy(anno.mappedBy());
        if (anno.targetEntity() != void.class)
            fmd.getElement().setDeclaredType(anno.targetEntity());
        setCascades(fmd.getElement(), anno.cascade());
        setOrphanRemoval(fmd.getElement(), anno.orphanRemoval());
        fmd.setAssociationType(FieldMetaData.ONE_TO_MANY);
    }

    /**
     * Parse @ManyToMany.
     */
    private void parseManyToMany(FieldMetaData fmd, ManyToMany anno) {
        switch (fmd.getDeclaredTypeCode()) {
            case JavaTypes.ARRAY:
            case JavaTypes.COLLECTION:
            case JavaTypes.MAP:
                if (fmd.getDeclaredType() == Properties.class || JavaTypes.maybePC(fmd.getElement()))
                    break;
                // no break
            default:
                throw new MetaDataException(_loc.get("bad-meta-anno", fmd,
                    "ManyToMany"));
        }

        fmd.setInDefaultFetchGroup(anno.fetch() == FetchType.EAGER);
        if (isMappingOverrideMode() && !StringUtil.isEmpty(anno.mappedBy()))
            fmd.setMappedBy(anno.mappedBy());
        if (anno.targetEntity() != void.class)
            fmd.getElement().setDeclaredType(anno.targetEntity());
        setCascades(fmd.getElement(), anno.cascade());
        fmd.setAssociationType(FieldMetaData.MANY_TO_MANY);
    }

    /**
     * Parse @MapKey.
     */
    private void parseMapKey(FieldMetaData fmd, MapKey anno) {
        String name = anno.name();
        if (StringUtil.isEmpty(name))
            fmd.getKey().setValueMappedBy(ValueMetaData.MAPPED_BY_PK);
        else
            fmd.getKey().setValueMappedBy(name);
    }

    /**
     * Parse @MapKeyClass.
     */
    private void parseMapKeyClass(FieldMetaData fmd, MapKeyClass anno) {
        if (anno.value() != void.class)
            fmd.getKey().setDeclaredType(anno.value());
        else
            throw new IllegalArgumentException(
                 "The value of the MapClassClass cannot be null");
    }

    /**
     * Parse @MapsId.
     */
    private void parseMapsId(FieldMetaData fmd, MapsId anno) {
        String value = anno.value();
        if (value != null)
            fmd.setMappedByIdValue(value);
        else
            fmd.setMappedByIdValue("");
    }

    /**
     * Setup the field as a LOB mapping.
     */
    protected void parseLobMapping(FieldMetaData fmd) {
    }

    /**
     * Parse @OrderBy.
     */
    private void parseOrderBy(FieldMetaData fmd, OrderBy anno) {
        String dec = anno.value();
        if (fmd.isElementCollection() &&
            fmd.getElement().getEmbeddedMetaData() != null) {
                if (dec.length() == 0 || dec.equals("ASC") ||
                    dec.equals("DESC"))
                    throw new MetaDataException(_loc.get(
                        "invalid-orderBy", fmd));
        }
        if (dec.length() == 0 || dec.equals("ASC"))
            dec = Order.ELEMENT + " asc";
        else if (dec.equals("DESC"))
            dec = Order.ELEMENT + " desc";

        fmd.setOrderDeclaration(dec);
    }

    /**
     * Parse @Persistent.
     */
    private void parsePersistent(FieldMetaData fmd, Persistent anno) {
        switch (fmd.getDeclaredTypeCode()) {
            case JavaTypes.ARRAY:
                if (fmd.getDeclaredType() == byte[].class
                    || fmd.getDeclaredType() == Byte[].class
                    || fmd.getDeclaredType() == char[].class
                    || fmd.getDeclaredType() == Character[].class)
                    break;
                // no break
            case JavaTypes.COLLECTION:
            case JavaTypes.MAP:
                throw new MetaDataException(_loc.get("bad-meta-anno", fmd,
                    "Persistent"));
        }

        if (!StringUtil.isEmpty(anno.mappedBy()))
            fmd.setMappedBy(anno.mappedBy());
        fmd.setInDefaultFetchGroup(anno.fetch() == FetchType.EAGER);
        if (!anno.optional())
            fmd.setNullValue(FieldMetaData.NULL_EXCEPTION);
        setCascades(fmd, anno.cascade());
        if (anno.embedded()) {
            if (!JavaTypes.maybePC(fmd.getValue()))
                throw new MetaDataException(_loc.get("bad-meta-anno", fmd,
                    "Persistent(embedded=true)"));
            fmd.setEmbedded(true);
            if (fmd.getEmbeddedMetaData() == null) {
                fmd.addEmbeddedMetaData(getAccessCode(fmd.getDeclaredType()));
            }
        }
    }

    /**
     * Parse @PersistentCollection.
     */
    private void parsePersistentCollection(FieldMetaData fmd,
        PersistentCollection anno) {
        if (fmd.getDeclaredTypeCode() != JavaTypes.ARRAY
            && fmd.getDeclaredTypeCode() != JavaTypes.COLLECTION)
            throw new MetaDataException(_loc.get("bad-meta-anno", fmd,
                "PersistentCollection"));

        fmd.setPersistentCollection(true);

        if (!StringUtil.isEmpty(anno.mappedBy()))
            fmd.setMappedBy(anno.mappedBy());
        fmd.setInDefaultFetchGroup(anno.fetch() == FetchType.EAGER);
        if (anno.elementType() != void.class)
            fmd.getElement().setDeclaredType(anno.elementType());
        setCascades(fmd.getElement(), anno.elementCascade());
        if (anno.elementEmbedded()) {
            if (!JavaTypes.maybePC(fmd.getElement()))
                throw new MetaDataException(_loc.get("bad-meta-anno", fmd,
                    "PersistentCollection(embeddedElement=true)"));
            fmd.getElement().setEmbedded(true);
            if (fmd.getElement().getEmbeddedMetaData() == null) {
                fmd.getElement().addEmbeddedMetaData(
                    getAccessCode(fmd.getElement().getDeclaredType()));
            }
        }
    }

    /**
     * Parse @ElementCollection.
     */
    private void parseElementCollection(FieldMetaData fmd,
        ElementCollection anno) {
        // TODO: throw exception if the runtime env is OpenJpa 1.x

        if (fmd.getDeclaredTypeCode() != JavaTypes.COLLECTION &&
            fmd.getDeclaredTypeCode() != JavaTypes.MAP)
            throw new MetaDataException(_loc.get("bad-meta-anno", fmd,
                "ElementCollection"));

        if (anno.targetClass() != void.class)
            fmd.getElement().setDeclaredType(anno.targetClass());
        fmd.setInDefaultFetchGroup(anno.fetch() == FetchType.EAGER);
        fmd.setElementCollection(true);
        ValueMetaData elem = fmd.getElement();
        boolean isEnum = elem.getDeclaredType().isEnum();
        if (!isEnum && JavaTypes.maybePC(elem)) {
            elem.setEmbedded(true);
            if (elem.getEmbeddedMetaData() == null)
                elem.addEmbeddedMetaData(getAccessCode(elem.getDeclaredType()));
        }
    }

    /**
     * Parse @PersistentMap.
     */
    private void parsePersistentMap(FieldMetaData fmd, PersistentMap anno) {
        if (fmd.getDeclaredTypeCode() != JavaTypes.MAP)
            throw new MetaDataException(_loc.get("bad-meta-anno", fmd,
                "PersistentMap"));

        fmd.setInDefaultFetchGroup(anno.fetch() == FetchType.EAGER);
        if (anno.keyType() != void.class)
            fmd.getKey().setDeclaredType(anno.keyType());
        if (anno.elementType() != void.class)
            fmd.getElement().setDeclaredType(anno.elementType());
        setCascades(fmd.getKey(), anno.keyCascade());
        setCascades(fmd.getElement(), anno.elementCascade());
        if (anno.keyEmbedded()) {
            if (!JavaTypes.maybePC(fmd.getKey()))
                throw new MetaDataException(_loc.get("bad-meta-anno", fmd,
                    "PersistentMap(embeddedKey=true)"));
            fmd.getKey().setEmbedded(true);
            if (fmd.getKey().getEmbeddedMetaData() == null) {
                fmd.getKey().addEmbeddedMetaData(
                    getAccessCode(fmd.getKey().getDeclaredType()));
            }
        }
        if (anno.elementEmbedded()) {
            if (!JavaTypes.maybePC(fmd.getElement()))
                throw new MetaDataException(_loc.get("bad-meta-anno", fmd,
                    "PersistentMap(embeddedValue=true)"));
            fmd.getElement().setEmbedded(true);
            if (fmd.getElement().getEmbeddedMetaData() == null)
                fmd.getElement().addEmbeddedMetaData(
                    getAccessCode(fmd.getElement().getDeclaredType()));
        }
    }

    /**
     * Set cascades on relation.
     */
    private void setCascades(ValueMetaData vmd, CascadeType[] cascades) {
        for (CascadeType cascade : cascades) {
            if (cascade == CascadeType.ALL || cascade == CascadeType.REMOVE)
                vmd.setCascadeDelete(ValueMetaData.CASCADE_IMMEDIATE);
            if (cascade == CascadeType.ALL || cascade == CascadeType.PERSIST)
                vmd.setCascadePersist(ValueMetaData.CASCADE_IMMEDIATE, false);
            if (cascade == CascadeType.ALL || cascade == CascadeType.MERGE)
                vmd.setCascadeAttach(ValueMetaData.CASCADE_IMMEDIATE);
            if (cascade == CascadeType.ALL || cascade == CascadeType.DETACH)
                vmd.setCascadeDetach(ValueMetaData.CASCADE_IMMEDIATE);
            if (cascade == CascadeType.ALL || cascade == CascadeType.REFRESH)
                vmd.setCascadeRefresh(ValueMetaData.CASCADE_IMMEDIATE);
        }
    }

    private void setOrphanRemoval(ValueMetaData vmd, boolean orphanRemoval) {
        if (orphanRemoval)
            vmd.setCascadeDelete(ValueMetaData.CASCADE_AUTO);
    }

    /**
     * Parse @SequenceGenerator.
     */
    private void parseSequenceGenerator(AnnotatedElement el,
        SequenceGenerator gen) {
        String name = gen.name();
        if (StringUtil.isEmpty(name))
            throw new MetaDataException(_loc.get("no-seq-name", el));

        if (_log.isTraceEnabled())
            _log.trace(_loc.get("parse-sequence", name));

        SequenceMetaData meta = getRepository().getCachedSequenceMetaData
            (name);
        if (meta != null) {
            if (_log.isWarnEnabled())
                _log.warn(_loc.get("dup-sequence", name, el));
            return;
        }

        // create new sequence
        meta = getRepository().addSequenceMetaData(name);
        String seq = gen.sequenceName();
        // Do not normalize the sequence name if it appears to be a plugin
        if (seq.indexOf('(') == -1){
            seq = normalizeSequenceName(seq);
        }
        int initial = gen.initialValue();
        int allocate = gen.allocationSize();
        String schema = normalizeSchemaName(gen.schema());
        String catalog = normalizeCatalogName(gen.catalog());
        // don't allow initial of 0 b/c looks like def value
        if (initial == 0)
            initial = 1;

        // create plugin string from info
        String clsName, props;
        if (StringUtil.isEmpty(seq)) {
            clsName = SequenceMetaData.IMPL_NATIVE;
            props = null;
        } else if (seq.indexOf('(') != -1) // plugin
        {
            clsName = Configurations.getClassName(seq);
            props = Configurations.getProperties(seq);
            seq = null;
        } else {
            clsName = SequenceMetaData.IMPL_NATIVE;
            props = null;
        }

        meta.setSequencePlugin(Configurations.getPlugin(clsName, props));
        meta.setSequence(seq);
        meta.setInitialValue(initial);
        meta.setAllocate(allocate);
        meta.setSchema(schema);
        meta.setCatalog(catalog);
        meta.setSource(getSourceFile(), (el instanceof Class) ? el : null,
            SourceTracker.SRC_ANNOTATIONS);
    }

    /**
     * Parse @NamedQuery.
     */
    private void parseNamedQueries(AnnotatedElement el, NamedQuery... queries) {
        QueryMetaData meta;
        for (NamedQuery query : queries) {
            if (StringUtil.isEmpty(query.name()))
                throw new MetaDataException(_loc.get("no-query-name", el));
            if (StringUtil.isEmpty(query.query()))
                throw new MetaDataException(_loc.get("no-query-string",
                    query.name(), el));

            if (_log.isTraceEnabled())
                _log.trace(_loc.get("parse-query", query.name()));

            meta = getRepository().searchQueryMetaDataByName(query.name());
            if (meta != null) {
            	Class<?> definingType = meta.getDefiningType();
                if ((definingType == null || definingType != _cls)
                  && _log.isWarnEnabled()) {
                    _log.warn(_loc.get("dup-query", query.name(), el,
                    		definingType));
                }
                continue;
            }
            meta = getRepository().addQueryMetaData(_cls, query.name());
            meta.setLanguage(JPQLParser.LANG_JPQL);
            meta.setQueryString(query.query());
            for (QueryHint hint : query.hints())
                meta.addHint(hint.name(), hint.value());
            LockModeType lmt = processNamedQueryLockModeType(query);
            if (lmt != null && lmt != LockModeType.NONE) {
                meta.addHint("openjpa.FetchPlan.ReadLockMode", lmt);
            }

            meta.setSource(getSourceFile(), (el instanceof Class) ? el : null,
                SourceTracker.SRC_ANNOTATIONS, getSourceFile() == null ? "" : getSourceFile().getPath());
            if (isMetaDataMode())
                meta.setSourceMode(MODE_META);
            else if (isMappingMode())
                meta.setSourceMode(MODE_MAPPING);
            else
                meta.setSourceMode(MODE_QUERY);
        }
    }

    /**
     * A private worker method that calculates the lock mode for an individual NamedQuery. If the NamedQuery is
     * configured to use the NONE lock mode(explicit or implicit), this method will promote the lock to a READ
     * level lock. This was done to allow for JPA1 apps to function properly under a 2.0 runtime.
     */
    private LockModeType processNamedQueryLockModeType(NamedQuery query) {
        LockModeType lmt = query.lockMode();
        if (query.lockMode() != null) {
            String lm = _conf.getLockManager();
            boolean optimistic = _conf.getOptimistic();
            if (lm != null) {
                lm = lm.toLowerCase();
                if (lm.contains("pessimistic")) {
                    if (lmt == LockModeType.NONE && !optimistic) {
                        if (_log.isWarnEnabled()) {
                            _log.warn(_loc.get("override-named-query-lock-mode", new String[] { "annotation",
                                query.name(), _cls.getName() }));
                        }
                        lmt = LockModeType.READ;
                    }
                }
            }
        }
        return lmt;
    }

    /**
     * Parse @NamedNativeQuery.
     */
    private void parseNamedNativeQueries(AnnotatedElement el,
        NamedNativeQuery... queries) {
        QueryMetaData meta;
        for (NamedNativeQuery query : queries) {
            if (StringUtil.isEmpty(query.name()))
                throw new MetaDataException(_loc.get("no-native-query-name",
                    el));
            if (StringUtil.isEmpty(query.query()))
                throw new MetaDataException(_loc.get("no-native-query-string",
                    query.name(), el));

            if (_log.isTraceEnabled())
                _log.trace(_loc.get("parse-native-query", query.name()));

            meta = getRepository().searchQueryMetaDataByName(query.name());
            if (meta != null) {
            	Class<?> defType = meta.getDefiningType();
                if ((defType != _cls) && _log.isWarnEnabled()) {
                    _log.warn(_loc.get("dup-query", query.name(), el, defType));
                }
                continue;
            }

            meta = getRepository().addQueryMetaData(null, query.name());
            meta.setLanguage(QueryLanguages.LANG_SQL);
            meta.setQueryString(query.query());
            Class<?> res = query.resultClass();
            if (ImplHelper.isManagedType(getConfiguration(), res))
                meta.setCandidateType(res);
            else if (!void.class.equals(res))
                meta.setResultType(res);

            if (!StringUtil.isEmpty(query.resultSetMapping()))
                meta.setResultSetMappingName(query.resultSetMapping());
            for (QueryHint hint : query.hints())
                meta.addHint(hint.name(), hint.value());

            meta.setSource(getSourceFile(), (el instanceof Class) ? el : null,
                SourceTracker.SRC_ANNOTATIONS, getSourceFile() == null ? "" : getSourceFile().getPath());
            if (isMetaDataMode())
                meta.setSourceMode(MODE_META);
            else if (isMappingMode())
                meta.setSourceMode(MODE_MAPPING);
            else
                meta.setSourceMode(MODE_QUERY);
        }
    }

    /**
     * Set the explicit access type, if specified.
     */
    private void parseAccess(FieldMetaData meta, Access access) {
    	if (access != null) {
    		meta.setAccessType(AccessCode.EXPLICIT
            | (access.value() == AccessType.FIELD ?
            		AccessCode.FIELD : AccessCode.PROPERTY));
    	}
    }

    private static class MethodKey {

        private final Method _method;

        public MethodKey(Method m) {
            _method = m;
        }

        @Override
        public int hashCode() {
            int code = 46 * 12 + _method.getName().hashCode();
            for (Class<?> param : _method.getParameterTypes())
                code = 46 * code + param.hashCode();
            return code;
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof MethodKey))
                return false;
            Method other = ((MethodKey) o)._method;
            if (!_method.getName().equals(other.getName()))
                return false;
            return Arrays.equals(_method.getParameterTypes(),
                other.getParameterTypes());
        }
    }

    private static class MethodComparator implements Comparator<Method> {

        private static MethodComparator INSTANCE = null;

        public static MethodComparator getInstance() {
            if (INSTANCE == null)
                INSTANCE = new MethodComparator();
            return INSTANCE;
        }

        @Override
        public int compare(Method m1, Method m2) {
            Class<?> c1 = m1.getDeclaringClass();
            Class<?> c2 = m2.getDeclaringClass();
            if (!c1.equals(c2)) {
                if (c1.isAssignableFrom(c2))
                    return -1;
                else
                    return 1;
            }
            int compare = m1.getName().compareTo(m2.getName ());
            if (compare != 0) {
                return compare;
            }

            Class<?>[] params1 = m1.getParameterTypes();
            Class<?>[] params2 = m2.getParameterTypes();
            compare = params1.length - params2.length;
            if (compare != 0) {
                return compare;
            }

            // Just using the Method#hashCode() is not enough as it only contains
            // the hash of the class + the hash of the NAME of the method...
            // Thus if they have the same number of parameters, we need to compare them all
            for (int i = 0; i < params1.length; i++) {
                compare = params1[i].hashCode() - params2[i].hashCode();
                if (compare != 0) {
                    return compare;
                }
            }

            return 0;
        }
    }

    /**
     * An internal class used to mimic the FetchGroup annotation.
     * This is needed to process the fetch-group element in xml
     * metadata with common code for the annotation.
     */
    static class FetchGroupImpl {
        private String name = "";
        private boolean postLoad = false;
        private FetchAttributeImpl[] attributes = {};
        private String[] fetchGroups = {};

        FetchGroupImpl(String name, boolean postLoad)
        {
            this.name = name;
            this.postLoad = postLoad;
        }

        public String name() {
            return name;
        }

        public boolean postLoad() {
            return postLoad;
        }

        public FetchAttributeImpl[] attributes() {
            return attributes;
        }

        public String[] fetchGroups() {
            return fetchGroups;
        }


        public void setAttributes(FetchAttributeImpl[] attributes) {
            this.attributes = attributes;
        }

        public void setFetchGroups(String[] fetchGroups) {
            this.fetchGroups = fetchGroups;
        }
    }

    /**
     * An internal class used to mimic the FetchAttribute annotation.
     * This is needed to process the fetch-attribute element in xml
     * metadata with common code for the annotation.
     */
    static class FetchAttributeImpl {
        private String name = "";
        private int recursionDepth = Integer.MIN_VALUE;

        public FetchAttributeImpl(String name, int recursionDepth) {
            this.name = name;
            this.recursionDepth = recursionDepth;
        }

        public String name() {
            return name;
        }

        public int recursionDepth() {
            return recursionDepth;
        }


    }

    protected String normalizeSequenceName(String seqName) {
        return seqName;
    }

    protected String normalizeSchemaName(String schName) {
        return schName;
    }

    protected String normalizeCatalogName(String catName) {
        return catName;
    }


    protected MultiQueryMetaData.Parameter.Mode toKernelParameterMode(ParameterMode mode) {
        switch (mode) {
            case IN : return MultiQueryMetaData.Parameter.Mode.IN;
            case OUT: return MultiQueryMetaData.Parameter.Mode.OUT;
            case INOUT: return MultiQueryMetaData.Parameter.Mode.INOUT;
            case REF_CURSOR: return MultiQueryMetaData.Parameter.Mode.CURSOR;
            default : return MultiQueryMetaData.Parameter.Mode.IN;
        }
    }

    protected void addSourceInfo(AnnotatedElement el, QueryMetaData meta) {
        meta.setSource(getSourceFile(), (el instanceof Class) ? el : null,
                SourceTracker.SRC_ANNOTATIONS, getSourceFile() == null ? "" : getSourceFile().getPath());
        if (isMetaDataMode())
            meta.setSourceMode(MODE_META);
        else if (isMappingMode())
            meta.setSourceMode(MODE_MAPPING);
        else
            meta.setSourceMode(MODE_QUERY);
    }

    protected void addHints(QueryMetaData meta, QueryHint...hints) {
        for (QueryHint hint : hints)
            meta.addHint(hint.name(), hint.value());

    }

    private void parseNamedStoredProcedureQueries(AnnotatedElement el, NamedStoredProcedureQuery... procs) {
        for (NamedStoredProcedureQuery proc : procs) {
            if (StringUtil.isEmpty(proc.name()))
                throw new MetaDataException(_loc.get("stored-proc-no-name", el));
            if (StringUtil.isEmpty(proc.procedureName()))
                throw new MetaDataException(_loc.get("stored-proc-no-dbname", el));

            // Query metadata name
            MultiQueryMetaData meta = new MultiQueryMetaData(_cls, proc.name(), proc.procedureName(), false);
            QueryMetaData existing = getRepository().addQueryMetaData(meta);
            if (existing != null && existing.getDefiningType() != meta.getDefiningType()) {
                getLog().warn(_loc.get("dup-query", meta.getName(), el, existing.getDefiningType()));
            }

            // Important: The query string is the name of the database stored procedure
            meta.setQueryString(proc.procedureName());

            // For each mapping name/result class, add a component metadata
            // The spec restricts that either ResultMappingName or ResultClasses be specified, but not both.
            // This is relevant because the order of mapping must match the order in which the
            // components are returned
            Class<?>[] resultClasses = proc.resultClasses();
            String[] resultSetMappings = proc.resultSetMappings();
            if (resultClasses.length > 0 && resultSetMappings.length > 0)
                throw new MetaDataException(_loc.get("stored-proc-both-mapping", el));
            for (Class<?> res : resultClasses) {
                meta.addComponent(res);
            }
            for (String mapping : resultSetMappings) {
                meta.addComponent(mapping);
            }
            StoredProcedureParameter[] params = proc.parameters();
            for (StoredProcedureParameter param : params) {
                MultiQueryMetaData.Parameter p = new MultiQueryMetaData.Parameter(
                        param.name(), param.type(), toKernelParameterMode(param.mode()));
                meta.registerParameter(p);
            }
            addHints(meta, proc.hints());
            addSourceInfo(el, meta);
        }
    }
}

