/*
 * 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.IOException;
import java.math.BigDecimal;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;

import jakarta.persistence.SharedCacheMode;
import jakarta.persistence.ValidationMode;
import jakarta.persistence.spi.PersistenceUnitInfo;
import jakarta.persistence.spi.PersistenceUnitTransactionType;

import org.apache.openjpa.conf.Compatibility;
import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.conf.OpenJPAConfigurationImpl;
import org.apache.openjpa.conf.OpenJPAProductDerivation;
import org.apache.openjpa.conf.Specification;
import org.apache.openjpa.datacache.DataCacheMode;
import org.apache.openjpa.kernel.MixedLockLevels;
import org.apache.openjpa.kernel.QueryHints;
import org.apache.openjpa.lib.conf.AbstractProductDerivation;
import org.apache.openjpa.lib.conf.Configuration;
import org.apache.openjpa.lib.conf.ConfigurationProvider;
import org.apache.openjpa.lib.conf.Configurations;
import org.apache.openjpa.lib.conf.MapConfigurationProvider;
import org.apache.openjpa.lib.conf.ProductDerivations;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.meta.XMLMetaDataParser;
import org.apache.openjpa.lib.meta.XMLVersionParser;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.MultiClassLoader;
import org.apache.openjpa.lib.util.StringUtil;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;


/**
 * Sets JPA specification defaults and parses JPA specification XML files.
 *
 * For globals, looks in <code>openjpa.properties</code> system property for
 * the location of a file to parse. If no system property is defined, the
 * default resource location of <code>META-INF/openjpa.xml</code> is used.
 *
 * For defaults, looks for <code>META-INF/persistence.xml</code>.
 * Within <code>persistence.xml</code>, look for the named persistence unit, or
 * if no name given, an OpenJPA unit (preferring an unnamed OpenJPA unit to
 * a named one).
 *
 * @author Abe White
 */
public class PersistenceProductDerivation
    extends AbstractProductDerivation
    implements OpenJPAProductDerivation {

    public static final Specification SPEC_JPA = new Specification("jpa 2");
    public static final Specification ALIAS_EJB = new Specification("ejb 3");
    public static final String RSRC_GLOBAL = "META-INF/openjpa.xml";
    public static final String RSRC_DEFAULT = "META-INF/persistence.xml";
    public static final BigDecimal VERSION_1_0 = BigDecimal.valueOf(1.0);

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

    private HashMap<String, PUNameCollision> _puNameCollisions
        = new HashMap<>();

    public static final String PREFIX = "jakarta.persistence";

    // These are properties that are invalid to be configured at the provider level.
    private static final String[] _invalidPersistenceProperties =
        new String[] { PREFIX + ".cache.storeMode", PREFIX + ".cache.retrieveMode" };

    private static Set<String> _hints = new HashSet<>();

    // Provider name to filter out PUs that don't belong to this derivation.
    protected String _providerImplName;

    static {
        _hints.add("jakarta.persistence.lock.timeout");
        _hints.add("jakarta.persistence.query.timeout");

        _hints.add("openjpa.FetchPlan.ExtendedPathLookup");
        _hints.add("openjpa.FetchBatchSize");
        _hints.add("openjpa.FetchPlan.FetchBatchSize");
        _hints.add("openjpa.MaxFetchDepth");
        _hints.add("openjpa.FetchPlan.MaxFetchDepth");
        _hints.add("openjpa.LockTimeout");
        _hints.add("openjpa.FetchPlan.LockTimeout");
        _hints.add("openjpa.QueryTimeout");
        _hints.add("openjpa.FetchPlan.QueryTimeout");
        _hints.add("openjpa.FlushBeforeQueries");
        _hints.add("openjpa.FetchPlan.FlushBeforeQueries");
        _hints.add("openjpa.ReadLockLevel");
        _hints.add("openjpa.FetchPlan.ReadLockLevel");
        _hints.add("openjpa.WriteLockLevel");
        _hints.add("openjpa.FetchPlan.WriteLockLevel");
        _hints.add("openjpa.FetchPlan.FetchBatchSize");
        _hints.add("openjpa.FetchPlan.LockScope");
        _hints.add("openjpa.FetchPlan.LockTimeout");
        _hints.add("openjpa.FetchPlan.MaxFetchDepth");
        _hints.add("openjpa.FetchPlan.QueryTimeout");
        _hints.add("openjpa.FetchPlan.ReadLockMode");
        _hints.add("openjpa.FetchPlan.WriteLockMode");
        _hints.add(QueryHints.HINT_AGGREGATE_LISTENER);
        _hints.add(QueryHints.HINT_AGGREGATE_LISTENERS);
        _hints.add(QueryHints.HINT_FILTER_LISTENER);
        _hints.add(QueryHints.HINT_FILTER_LISTENERS);
        _hints.add(QueryHints.HINT_IGNORE_FINDER);
        _hints.add(QueryHints.HINT_IGNORE_PREPARED_QUERY);
        _hints.add(QueryHints.HINT_INVALIDATE_FINDER);
        _hints.add(QueryHints.HINT_INVALIDATE_PREPARED_QUERY);
        _hints.add(QueryHints.HINT_PARAM_MARKER_IN_QUERY);
        _hints.add(QueryHints.HINT_RECACHE_FINDER);
        _hints.add(QueryHints.HINT_RESULT_COUNT);
        _hints.add(QueryHints.HINT_SUBCLASSES);
        _hints.add(QueryHints.HINT_RELAX_BIND_PARAM_TYPE_CHECK);
        _hints.add(QueryHints.HINT_USE_LITERAL_IN_SQL);

        _hints = Collections.unmodifiableSet(_hints);
    }

	public PersistenceProductDerivation() {
		_providerImplName = PersistenceProviderImpl.class.getName();
	}

    @Override
    public void putBrokerFactoryAliases(Map<String, String> m) {
    }

    @Override
    public int getType() {
        return TYPE_SPEC;
    }

    @Override
    public String getConfigurationPrefix() {
        return PREFIX;
    }

    @Override
    public Set<String> getSupportedQueryHints() {
        return _hints;
    }

    @Override
    public void validate()
        throws Exception {
        // make sure JPA is available
        AccessController.doPrivileged(J2DoPrivHelper.getClassLoaderAction(
            jakarta.persistence.EntityManagerFactory.class));
    }

    @Override
    public boolean beforeConfigurationLoad(Configuration c) {
        if (!(c instanceof OpenJPAConfigurationImpl))
            return false;

        OpenJPAConfigurationImpl conf = (OpenJPAConfigurationImpl) c;
        conf.metaFactoryPlugin.setAlias(ALIAS_EJB.getName(), PersistenceMetaDataFactory.class.getName());
        conf.metaFactoryPlugin.setAlias(SPEC_JPA.getName(),  PersistenceMetaDataFactory.class.getName());

        conf.addValue(new EntityManagerFactoryValue());

        conf.readLockLevel.setAlias("optimistic", String.valueOf(MixedLockLevels.LOCK_OPTIMISTIC));
        conf.readLockLevel.setAlias("optimistic-force-increment", String
            .valueOf(MixedLockLevels.LOCK_OPTIMISTIC_FORCE_INCREMENT));
        conf.readLockLevel.setAlias("pessimistic-read", String
            .valueOf(MixedLockLevels.LOCK_PESSIMISTIC_READ));
        conf.readLockLevel.setAlias("pessimistic-write", String
            .valueOf(MixedLockLevels.LOCK_PESSIMISTIC_WRITE));
        conf.readLockLevel.setAlias("pessimistic-force-increment", String
            .valueOf(MixedLockLevels.LOCK_PESSIMISTIC_FORCE_INCREMENT));

        conf.writeLockLevel.setAlias("optimistic", String
            .valueOf(MixedLockLevels.LOCK_OPTIMISTIC));
        conf.writeLockLevel.setAlias("optimistic-force-increment", String
            .valueOf(MixedLockLevels.LOCK_OPTIMISTIC_FORCE_INCREMENT));
        conf.writeLockLevel.setAlias("pessimistic-read", String
            .valueOf(MixedLockLevels.LOCK_PESSIMISTIC_READ));
        conf.writeLockLevel.setAlias("pessimistic-write", String
            .valueOf(MixedLockLevels.LOCK_PESSIMISTIC_WRITE));
        conf.writeLockLevel.setAlias("pessimistic-force-increment", String
            .valueOf(MixedLockLevels.LOCK_PESSIMISTIC_FORCE_INCREMENT));


        configureBeanValidation(conf);

        conf.dataCacheMode = conf.addString(JPAProperties.CACHE_MODE);
        conf.dataCacheMode.setDefault(DataCacheMode.UNSPECIFIED.toString());
        conf.dataCacheMode.set(DataCacheMode.UNSPECIFIED.toString());

        return true;
    }

    /**
     * Bean Validation configuration is unusual because its usage of enums and keys that
     * do not have counterparts in kernel.
     * Hence the plugins are defined in product derivation instead of the kernel's
     * core configuration.
     *
     * @param conf
     */
    private void configureBeanValidation(OpenJPAConfigurationImpl conf) {
        // Validation defines/adds the following plugins to OpenJPA Configuration
        conf.validationFactory         = conf.addObject(JPAProperties.VALIDATE_FACTORY);
        conf.validator                 = conf.addObject("Validator");
        conf.validationMode            = conf.addString(JPAProperties.VALIDATE_MODE);
        conf.validationGroupPrePersist = conf.addString(JPAProperties.VALIDATE_PRE_PERSIST);
        conf.validationGroupPreUpdate  = conf.addString(JPAProperties.VALIDATE_PRE_UPDATE);
        conf.validationGroupPreRemove  = conf.addString(JPAProperties.VALIDATE_PRE_REMOVE);

        conf.validationMode.setDynamic(true);
        String[] aliases = new String[] {
                String.valueOf(ValidationMode.AUTO),
                String.valueOf(ValidationMode.AUTO).toLowerCase(Locale.ENGLISH),
                String.valueOf(ValidationMode.CALLBACK),
                String.valueOf(ValidationMode.CALLBACK).toLowerCase(Locale.ENGLISH),
                String.valueOf(ValidationMode.NONE),
                String.valueOf(ValidationMode.NONE).toLowerCase(Locale.ENGLISH)
        };
        conf.validationMode.setAliases(aliases);
        conf.validationMode.setAliasListComprehensive(true);
        conf.validationMode.setDefault(aliases[0]);

        conf.validationGroupPrePersist.setString(JPAProperties.VALIDATE_GROUP_DEFAULT);
        conf.validationGroupPrePersist.setDefault("");
        conf.validationGroupPrePersist.setDynamic(true);

        conf.validationGroupPreUpdate.setString(JPAProperties.VALIDATE_GROUP_DEFAULT);
        conf.validationGroupPreUpdate.setDefault("");
        conf.validationGroupPreUpdate.setDynamic(true);

        conf.validationGroupPreRemove.setDefault("");
        conf.validationGroupPreRemove.setDynamic(true);

        conf.validationFactory.setInstantiatingGetter("getValidationFactoryInstance");
        conf.validationFactory.setDynamic(true);

        conf.validator.setInstantiatingGetter("getValidatorInstance");
        conf.validator.setDynamic(true);
        conf.validator.makePrivate();
    }

    @Override
    public boolean afterSpecificationSet(Configuration c) {
        if (!OpenJPAConfigurationImpl.class.isInstance(c)
         && !SPEC_JPA.isSame(((OpenJPAConfiguration) c).getSpecification()))
            return false;

        OpenJPAConfigurationImpl conf = (OpenJPAConfigurationImpl) c;
        conf.metaFactoryPlugin.setDefault(SPEC_JPA.getName());
        conf.metaFactoryPlugin.setString(SPEC_JPA.getName());
        conf.nontransactionalWrite.setDefault("true");
        conf.nontransactionalWrite.set(true);
        Specification spec = ((OpenJPAConfiguration) c).getSpecificationInstance();
        int specVersion = spec.getVersion();
        Compatibility compatibility = conf.getCompatibilityInstance();
        spec.setCompatibility(compatibility);
        if (specVersion < 2) {
            compatibility.setFlushBeforeDetach(true);
            compatibility.setCopyOnDetach(true);
            compatibility.setPrivatePersistentProperties(true);
            compatibility.setIgnoreDetachedStateFieldForProxySerialization(true);
            // Disable bean validation for spec level < 2 configurations
            conf.validationMode.set(String.valueOf(ValidationMode.NONE));
        } else {
            compatibility.setAbstractMappingUniDirectional(true);
            compatibility.setNonDefaultMappingAllowed(true);
        }
        return true;
    }

    /**
     * Load configuration from the given persistence unit with the specified
     * user properties.
     */
    public ConfigurationProvider load(PersistenceUnitInfo pinfo, Map m)
        throws IOException {
        if (pinfo == null)
            return null;
        if (!isOpenJPAPersistenceProvider(pinfo, null)) {
            warnUnknownProvider(pinfo);
            return null;
        }

        ConfigurationProviderImpl cp = new ConfigurationProviderImpl();
        cp.addProperties(PersistenceUnitInfoImpl.toOpenJPAProperties(pinfo));
        cp.addProperties(m);
        if (pinfo instanceof PersistenceUnitInfoImpl) {
            PersistenceUnitInfoImpl impl = (PersistenceUnitInfoImpl) pinfo;
            if (impl.getPersistenceXmlFileUrl() != null)
                cp.setSource(impl.getPersistenceXmlFileUrl().toString());
        }
        return cp;
    }

    /**
     * Load configuration from the given resource and unit names, which may
     * be null.
     */
    public ConfigurationProvider load(String rsrc, String name, Map m)
        throws IOException {
        boolean explicit = !StringUtil.isEmpty(rsrc);
        if (!explicit)
            rsrc = RSRC_DEFAULT;

        ConfigurationProviderImpl cp = new ConfigurationProviderImpl();
        Boolean ret = load(cp, rsrc, name, m, null, explicit);
        if (ret != null)
            return (ret) ? cp : null;
        if (explicit)
            return null;

        // persistence.xml does not exist; just load map
        PersistenceUnitInfoImpl pinfo = new PersistenceUnitInfoImpl();
        pinfo.fromUserProperties(m);
        if (!isOpenJPAPersistenceProvider(pinfo, null)) {
            warnUnknownProvider(pinfo);
            return null;
        }
        cp.addProperties(pinfo.toOpenJPAProperties());
        return cp;
    }

    @Override
    public ConfigurationProvider load(String rsrc, String anchor,
        ClassLoader loader)
        throws IOException {
        if (rsrc != null && !rsrc.endsWith(".xml"))
            return null;
        ConfigurationProviderImpl cp = new ConfigurationProviderImpl();
        if (load(cp, rsrc, anchor, null, loader, true) == Boolean.TRUE)
            return cp;
        return null;
    }

    @Override
    public ConfigurationProvider load(File file, String anchor)
        throws IOException {
        if (!file.getName().endsWith(".xml"))
            return null;

        ConfigurationParser parser = new ConfigurationParser(null);
        parser.parse(file);
        return load(findUnit((List<PersistenceUnitInfoImpl>)
            parser.getResults(), anchor, null), null);
    }

    @Override
    public String getDefaultResourceLocation() {
        return RSRC_DEFAULT;
    }

    @Override
    public List<String> getAnchorsInFile(File file) throws IOException {
        ConfigurationParser parser = new ConfigurationParser(null);
        try {
            parser.parse(file);
            return getUnitNames(parser);
        } catch (IOException e) {
            // not all configuration files are XML; return null if unparsable
            return null;
        }
    }

    private List<String> getUnitNames(ConfigurationParser parser) {
        List<PersistenceUnitInfoImpl> units = parser.getResults();
        List<String> names = new ArrayList<>();
        for (PersistenceUnitInfoImpl unit : units){
        	String provider = unit.getPersistenceProviderClassName();
			// Only add the PU name if the provider it is ours or not specified.
			if (provider == null || provider.equals(_providerImplName)) {
				names.add(unit.getPersistenceUnitName());
			} else {
				// Should trace something, but logging isn't configured yet.
				// Swallow.
			}
        }
        return names;
    }

    @Override
    public List getAnchorsInResource(String resource) throws Exception {
        ConfigurationParser parser = new ConfigurationParser(null);
        try {
        	List results = new ArrayList();
            ClassLoader loader = AccessController.doPrivileged(
                J2DoPrivHelper.getContextClassLoaderAction());
            List<URL> urls = getResourceURLs(resource, loader);
            if (urls != null) {
                for (URL url : urls) {
                    parser.parse(url);
                    results.addAll(getUnitNames(parser));
                }
            }
            return results;
        } catch (IOException e) {
            // not all configuration files are XML; return null if unparsable
            return null;
        }
    }

    @Override
    public ConfigurationProvider loadGlobals(ClassLoader loader)
        throws IOException {
        String[] prefixes = ProductDerivations.getConfigurationPrefixes();
        String rsrc = null;
        for (int i = 0; i < prefixes.length && StringUtil.isEmpty(rsrc); i++)
           rsrc = AccessController.doPrivileged(J2DoPrivHelper
                .getPropertyAction(prefixes[i] + ".properties"));
        boolean explicit = !StringUtil.isEmpty(rsrc);
        String anchor = null;
        int idx = (!explicit) ? -1 : rsrc.lastIndexOf('#');
        if (idx != -1) {
            // separate name from <resrouce>#<name> string
            if (idx < rsrc.length() - 1)
                anchor = rsrc.substring(idx + 1);
            rsrc = rsrc.substring(0, idx);
        }
        if (StringUtil.isEmpty(rsrc))
            rsrc = RSRC_GLOBAL;
        else if (!rsrc.endsWith(".xml"))
            return null;

        ConfigurationProviderImpl cp = new ConfigurationProviderImpl();
        if (load(cp, rsrc, anchor, null, loader, explicit) == Boolean.TRUE)
            return cp;
        return null;
    }

    @Override
    public ConfigurationProvider loadDefaults(ClassLoader loader)
        throws IOException {
        ConfigurationProviderImpl cp = new ConfigurationProviderImpl();
        if (load(cp, RSRC_DEFAULT, null, null, loader, false) == Boolean.TRUE)
            return cp;
        return null;
    }

      /**
      * This method checks to see if the provided <code>puName</code> was
      * detected in multiple resources. If a collision is detected, a warning
      * will be logged and this method will return <code>true</code>.
      * <p>
      */
     public boolean checkPuNameCollisions(Log logger,String puName){
         PUNameCollision p = _puNameCollisions.get(puName);
         if (p != null){
             p.logCollision(logger);
             return true;
         }
         return false;
     }

    private static List<URL> getResourceURLs(String rsrc, ClassLoader loader)
        throws IOException {
        Enumeration<URL> urls = null;
        try {
            urls = AccessController.doPrivileged(J2DoPrivHelper.getResourcesAction(loader, rsrc));
            if (!urls.hasMoreElements()) {
                if (!rsrc.startsWith("META-INF"))
                  urls = AccessController.doPrivileged(J2DoPrivHelper.getResourcesAction(loader, "META-INF/" + rsrc));
                if (!urls.hasMoreElements())
                    return null;
            }
        } catch (PrivilegedActionException pae) {
            throw (IOException) pae.getException();
        }

        return Collections.list(urls);
    }

    /**
     * Looks through the resources at <code>rsrc</code> for a configuration
     * file that matches <code>name</code> (or an unnamed one if
     * <code>name</code> is <code>null</code>), and loads the XML in the
     * resource into a new {@link PersistenceUnitInfo}. Then, applies the
     * overrides in <code>m</code>.
     *
     * @return {@link Boolean#TRUE} if the resource was loaded, null if it
     * does not exist, or {@link Boolean#FALSE} if it is not for OpenJPA
     */
    private Boolean load(ConfigurationProviderImpl cp, String rsrc,
        String name, Map m, ClassLoader loader, boolean explicit)
        throws IOException {
        ClassLoader contextLoader = null;
        if (loader == null) {
            contextLoader = AccessController.doPrivileged(J2DoPrivHelper.getContextClassLoaderAction());
            loader = contextLoader;
        }

        List<URL> urls = getResourceURLs(rsrc, loader);
        if (urls == null || urls.size() == 0)
            return null;

        ConfigurationParser parser = new ConfigurationParser(m);
        PersistenceUnitInfoImpl pinfo = parseResources(parser, urls, name, loader);
        if (pinfo == null) {
            if (!explicit)
                return Boolean.FALSE;
            throw new MissingResourceException(_loc.get("missing-xml-config",
                rsrc, String.valueOf(name)).getMessage(), getClass().getName(), rsrc);
        } else if (!isOpenJPAPersistenceProvider(pinfo, loader)) {
            if (!explicit) {
                warnUnknownProvider(pinfo);
                return Boolean.FALSE;
            }
            throw new MissingResourceException(_loc.get("unknown-provider",
                rsrc, name, pinfo.getPersistenceProviderClassName()).
                getMessage(), getClass().getName(), rsrc);
        }

        // Process jar-file references after confirming OpenJPA is the desired JPA provider.
        if ( loader != contextLoader && loader instanceof MultiClassLoader) {
            // combine the MultiClassLoader and set to the context
            // so that it could be used in the jar validation
            MultiClassLoader mutliClassLoader = (MultiClassLoader) loader;
            contextLoader = (contextLoader != null) ? contextLoader
                    : AccessController.doPrivileged(J2DoPrivHelper.getContextClassLoaderAction());
            mutliClassLoader.addClassLoader(contextLoader);
            AccessController.doPrivileged(J2DoPrivHelper.setContextClassLoaderAction(mutliClassLoader));
        }
        pinfo.processJarFileNames();

        if (contextLoader != null) {
            //restore the context loader
            AccessController.doPrivileged(J2DoPrivHelper.setContextClassLoaderAction(contextLoader));
        }

        cp.addProperties(pinfo.toOpenJPAProperties());
        cp.setSource(pinfo.getPersistenceXmlFileUrl().toString());
        return Boolean.TRUE;
    }

    /**
     * Parse resources at the given location. Searches for a
     * PersistenceUnitInfo with the requested name, or an OpenJPA unit if
     * no name given (preferring an unnamed OpenJPA unit to a named one).
     */
    private PersistenceUnitInfoImpl parseResources(ConfigurationParser parser,
        List<URL> urls, String name, ClassLoader loader)
        throws IOException {
        List<PersistenceUnitInfoImpl> pinfos = new ArrayList<>();
        for (URL url : urls) {
            parser.parse(url);
            pinfos.addAll((List<PersistenceUnitInfoImpl>) parser.getResults());
        }
        return findUnit(pinfos, name, loader);
    }

    /**
     * Find the unit with the given name, or an OpenJPA unit if no name is
     * given (preferring an unnamed OpenJPA unit to a named one).
     */
    private PersistenceUnitInfoImpl findUnit(List<PersistenceUnitInfoImpl>
        pinfos, String name, ClassLoader loader) {
        PersistenceUnitInfoImpl ojpa = null;
        PersistenceUnitInfoImpl result = null;
        for (PersistenceUnitInfoImpl pinfo : pinfos) {
            // found named unit?
            if (name != null) {
                if (name.equals(pinfo.getPersistenceUnitName())){
                    if (result != null){
                        this.addPuNameCollision(name, result.getPersistenceXmlFileUrl().toString(),
                                pinfo.getPersistenceXmlFileUrl().toString());

                    } else {
                        // Grab a ref to the pinfo that matches the name we're
                        // looking for. Keep going to look for duplicate pu names.
                        result = pinfo;
                    }
                }
                continue;
            }

            if (isOpenJPAPersistenceProvider(pinfo, loader)) {
                // if no name given and found unnamed unit, return it.
                // otherwise record as default unit unless we find a better match later
                if (StringUtil.isEmpty(pinfo.getPersistenceUnitName()))
                    return pinfo;
                if (ojpa == null)
                    ojpa = pinfo;
            }
        }
        if(result!=null){
            return result;
        }
        return ojpa;
    }

    /**
     * Return whether the given persistence unit uses an OpenJPA provider.
     */
    private static boolean isOpenJPAPersistenceProvider(PersistenceUnitInfo pinfo, ClassLoader loader) {
        String provider = pinfo.getPersistenceProviderClassName();
        if (StringUtil.isEmpty(provider) || PersistenceProviderImpl.class.getName().equals(provider))
            return true;

        if (loader == null)
            loader = AccessController.doPrivileged(J2DoPrivHelper.getContextClassLoaderAction());
        try {
            if (PersistenceProviderImpl.class.isAssignableFrom(Class.forName(provider, false, loader)))
                return true;
        } catch (Throwable t) {
            log(_loc.get("unloadable-provider", provider, t).getMessage());
            return false;
        }
        return false;
    }

    /**
     * Warn the user that we could only find an unrecognized persistence
     * provider.
     */
    private static void warnUnknownProvider(PersistenceUnitInfo pinfo) {
        log(_loc.get("unrecognized-provider", pinfo.getPersistenceProviderClassName()).getMessage());
    }

    /**
     * Log a message.
     */
    private static void log(String msg) {
        // at this point logging isn't configured yet
        // START - ALLOW PRINT STATEMENTS
        System.err.println(msg);
        // STOP - ALLOW PRINT STATEMENTS
    }

    private void addPuNameCollision(String puName, String file1, String file2){
        PUNameCollision pun = _puNameCollisions.get(puName);
        if (pun != null){
            pun.addCollision(file1, file2);
        } else {
            _puNameCollisions.put(puName, new PUNameCollision(puName, file1, file2));
        }
    }

    /**
     * Custom configuration provider.
     */
    public static class ConfigurationProviderImpl
        extends MapConfigurationProvider {

        private String _source;

        public ConfigurationProviderImpl() {
        }

        public ConfigurationProviderImpl(Map props) {
            super(props);
        }

        /**
         * Set the source of information in this provider.
         */
        public void setSource(String source) {
            _source = source;
        }

        @Override
        public void setInto(Configuration conf) {
            if (conf instanceof OpenJPAConfiguration) {
                OpenJPAConfiguration oconf = (OpenJPAConfiguration) conf;
                Object persistenceVersion = getProperties().get(PersistenceUnitInfoImpl.PERSISTENCE_VERSION);
                if (persistenceVersion == null) {
                    oconf.setSpecification(SPEC_JPA);
                } else {
                    // Set the spec level based on the persistence version
                    oconf.setSpecification("jpa " + persistenceVersion.toString());
                }


                // we merge several persistence.xml elements into the
                // MetaDataFactory property implicitly.  if the user has a
                // global openjpa.xml with this property set, its value will
                // get overwritten by our implicit setting.  so instead, combine
                // the global value with our settings
                String orig = oconf.getMetaDataFactory();
                if (!StringUtil.isEmpty(orig)) {
                    String key = ProductDerivations.getConfigurationKey("MetaDataFactory", getProperties());
                    Object override = getProperties().get(key);
                    if (override instanceof String)
                        addProperty(key, Configurations.combinePlugins(orig, (String) override));
                }
            }
            super.setInto(conf, null);

            // At this point user properties have been loaded into the configuration. Apply any modifications based off
            // those.
            if (conf instanceof OpenJPAConfiguration) {
                OpenJPAConfiguration oconf = (OpenJPAConfiguration) conf;
                String dataCache = oconf.getDataCache();
                String sharedDataCacheMode = oconf.getDataCacheMode();

                if (DataCacheMode.NONE.toString().equals(sharedDataCacheMode)
                        && dataCache != null && !"false".equals(dataCache)) {
                    Log log = conf.getConfigurationLog();
                    if (log.isWarnEnabled()) {
                        log.warn(_loc.get("shared-cache-mode-take-precedence", dataCache));
                    }
                }

                if ((dataCache == null || "false".equals(dataCache))
                        && !DataCacheMode.NONE.toString().equals(sharedDataCacheMode)
                        && !DataCacheMode.UNSPECIFIED.toString().equals(sharedDataCacheMode)){
                    oconf.setDataCache("true");
                }

                // If the datacache is enabled, make sure we have a RemoteCommitProvider
                String rcp = oconf.getRemoteCommitProvider();
                dataCache = oconf.getDataCache();
                // If the datacache is set and is something other than false
                if (dataCache != null && !"false".equals(dataCache)) {
                    // If RCP is null or empty, set it to sjvm.
                    if (rcp == null || "".equals(rcp)) {
                        oconf.setRemoteCommitProvider("sjvm");
                    }
                }
            }

            Log log = conf.getConfigurationLog();
            if (log.isWarnEnabled()) {
                Map<?, ?> props = getProperties();
                for (String propKey : _invalidPersistenceProperties) {
                    Object propValue = props.get(propKey);
                    if (propValue != null) {
                        log.warn(_loc.get("invalid-persistence-property", new Object[] { propKey, propValue }));
                    }
                }
            }
            if (log.isTraceEnabled()) {
                String src = (_source == null) ? "?" : _source;
                log.trace(_loc.get("conf-load", src, getProperties()));
            }
        }
    }

    /**
     * SAX handler capable of parsing an JPA persistence.xml file.
     * Package-protected for testing.
     */
    public static class ConfigurationParser
        extends XMLMetaDataParser {

        private static final String PERSISTENCE_XSD_2_0 = "persistence_2_0.xsd";
        private static final String PERSISTENCE_XSD_2_1 = "persistence_2_1.xsd";
        private static final String PERSISTENCE_XSD_2_2 = "persistence_2_2.xsd";
        private static final String PERSISTENCE_XSD_3_0 = "persistence_3_0.xsd";

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

        private final Map _map;
        private PersistenceUnitInfoImpl _info = null;
        private URL _source = null;
        private String _persistenceVersion;
        private String _schemaLocation;
        private boolean _excludeUnlistedSet = false;

        public ConfigurationParser(Map map) {
            _map = map;
            setCaching(false);
            setValidating(true);
            setParseText(true);
        }

