/*
 * 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.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import jakarta.persistence.SharedCacheMode;
import jakarta.persistence.ValidationMode;
import jakarta.persistence.spi.ClassTransformer;
import jakarta.persistence.spi.PersistenceUnitInfo;
import jakarta.persistence.spi.PersistenceUnitTransactionType;
import javax.sql.DataSource;

import org.apache.openjpa.lib.conf.Configuration;
import org.apache.openjpa.lib.conf.Configurations;
import org.apache.openjpa.lib.conf.ProductDerivations;
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.MultiClassLoader;
import org.apache.openjpa.util.ClassResolver;

/**
 * Implementation of the {@link PersistenceUnitInfo} interface used by OpenJPA
 * when parsing persistence configuration information.
 *
 */
public class PersistenceUnitInfoImpl
    implements PersistenceUnitInfo, SourceTracker {

    public static final String PERSISTENCE_VERSION = "PersistenceVersion";


    private static final Localizer s_loc = Localizer.forPackage
        (PersistenceUnitInfoImpl.class);

    private String _name;
    private final HashMap<Object,Object> _props = new HashMap<>();
    private PersistenceUnitTransactionType _transType =
        PersistenceUnitTransactionType.RESOURCE_LOCAL;

    private String _providerClassName;
    private List<String> _mappingFileNames;
    private List<String> _entityClassNames;
    private List<URL> _jarFiles;
    private List<String> _jarFileNames;
    private String _jtaDataSourceName;
    private DataSource _jtaDataSource;
    private String _nonJtaDataSourceName;
    private DataSource _nonJtaDataSource;
    private boolean _excludeUnlisted;
    private URL _persistenceXmlFile;
    private String _schemaVersion = "1.0";
    private ValidationMode _validationMode;
    private SharedCacheMode _sharedCacheMode;

    // A persistence unit is defined by a persistence.xml file. The jar
    // file or directory whose META-INF directory contains the
    // persistence.xml file is termed the root of the persistence unit.
    //
    // In Java EE, the root of a persistence unit may be one of the following:
    // - an EJB-JAR file
    // - the WEB-INF/classes directory of a WAR file[38]
    // - a jar file in the WEB-INF/lib directory of a WAR file
    // - a jar file in the root of the EAR
    // - a jar file in the EAR library directory
    // - an application client jar file
    private URL _root;

    @Override
    public ClassLoader getClassLoader() {
        return null;
    }

    @Override
    public ClassLoader getNewTempClassLoader() {
        return AccessController.doPrivileged(J2DoPrivHelper
            .newTemporaryClassLoaderAction(AccessController
                .doPrivileged(J2DoPrivHelper.getContextClassLoaderAction())));
    }

    @Override
    public String getPersistenceUnitName() {
        return _name;
    }

    public void setPersistenceUnitName(String emName) {
        _name = emName;
    }

    @Override
    public String getPersistenceProviderClassName() {
        return _providerClassName;
    }

    public void setPersistenceProviderClassName(String providerClassName) {
        _providerClassName = providerClassName;
    }

    @Override
    public PersistenceUnitTransactionType getTransactionType() {
        return _transType;
    }

    public void setTransactionType(PersistenceUnitTransactionType transType) {
        _transType = transType;
    }

    public String getJtaDataSourceName() {
        return _jtaDataSourceName;
    }

    public void setJtaDataSourceName(String jta) {
        _jtaDataSourceName = jta;
        if (jta != null)
            _jtaDataSource = null;
    }

    @Override
    public DataSource getJtaDataSource() {
        return _jtaDataSource;
    }

    public void setJtaDataSource(DataSource ds) {
        _jtaDataSource = ds;
        if (ds != null)
            _jtaDataSourceName = null;
    }

    public String getNonJtaDataSourceName() {
        return _nonJtaDataSourceName;
    }

    public void setNonJtaDataSourceName(String nonJta) {
        _nonJtaDataSourceName = nonJta;
        if (nonJta != null)
            _nonJtaDataSource = null;
    }

    @Override
    public DataSource getNonJtaDataSource() {
        return _nonJtaDataSource;
    }

    public void setNonJtaDataSource(DataSource ds) {
        _nonJtaDataSource = ds;
        if (ds != null)
            _nonJtaDataSourceName = null;
    }

    @Override
    public URL getPersistenceUnitRootUrl() {
        return _root;
    }

    public void setPersistenceUnitRootUrl(URL root) {
        _root = root;
    }

    @Override
    public boolean excludeUnlistedClasses() {
        return _excludeUnlisted;
    }

    public void setExcludeUnlistedClasses(boolean excludeUnlisted) {
        _excludeUnlisted = excludeUnlisted;
    }

    @Override
    public List<String> getMappingFileNames() {
        if (_mappingFileNames == null)
            return Collections.emptyList();
        return _mappingFileNames;
    }

    public void addMappingFileName(String name) {
        if (_mappingFileNames == null)
            _mappingFileNames = new ArrayList<>();
        _mappingFileNames.add(name);
    }

    @Override
    public List<URL> getJarFileUrls() {
        if (_jarFiles == null)
            return Collections.emptyList();
        return _jarFiles;
    }

    public void addJarFile(URL jar) {
        if (_jarFiles == null)
            _jarFiles = new ArrayList<>();
        _jarFiles.add(jar);
    }

    public void addJarFileName(String name) {
        // Defer searching the classpath for jar files referenced by the jar-file element until after
        // the XML has been parsed and it has been confirmed that OpenJPA is the desired JPA provider.

        if (_jarFileNames == null) {
            _jarFileNames = new ArrayList<>();
        }
        _jarFileNames.add(name);
    }

    /**
     * Process jar-file elements. An IllegalArgumentException may be thrown if the jar file does not exist in the
     * classpath.
     */
    public void processJarFileNames() {
        if (_jarFileNames != null) {
            for (String name : _jarFileNames) {
                validateJarFileName(name);
            }

            _jarFileNames.clear();
        }
    }

    public void validateJarFileName(String name) {
        ClassLoader contextClassLoader = AccessController.doPrivileged(J2DoPrivHelper.getContextClassLoaderAction());
        MultiClassLoader loader = AccessController
            .doPrivileged(J2DoPrivHelper.newMultiClassLoaderAction());
        loader.addClassLoader(contextClassLoader);
        loader.addClassLoader(getClass().getClassLoader());
        loader.addClassLoader(MultiClassLoader.THREAD_LOADER);
        URL url = AccessController.doPrivileged(
            J2DoPrivHelper.getResourceAction(loader, name));
        if (url != null) {
            addJarFile(url);
            return;
        }

        // jar file is not a resource; check classpath
        String classPath = null;

        //first check if the classpath is set from ant class loader
        if (contextClassLoader instanceof MultiClassLoader) {
            for (ClassLoader classLoader : ((MultiClassLoader) contextClassLoader).getClassLoaders()){
                try {
                    Method getClassPathMethod = classLoader.getClass().getMethod("getClasspath", new Class[]{});
                    classPath = (String) getClassPathMethod.invoke(classLoader, new Object[]{});
                    if (classPath != null)
                        break;
                } catch (Exception e) {
                    //do nothing
                }
            }
        }

        if (classPath == null) {
            classPath = AccessController.doPrivileged(
                    J2DoPrivHelper.getPropertyAction("java.class.path"));
        }
        String[] cp = classPath.split(J2DoPrivHelper.getPathSeparator());

        for (String s : cp) {
            if (s.equals(name)
                    || s.endsWith(File.separatorChar + name)) {
                try {
                    addJarFile(AccessController
                            .doPrivileged(J2DoPrivHelper
                                    .toURLAction(new File(s))));
                    return;
                }
                catch (PrivilegedActionException | MalformedURLException pae) {
                    break;
                }
            }
        }
        throw new IllegalArgumentException(s_loc.get("bad-jar-name", name).
            getMessage());
    }

    @Override
    public List<String> getManagedClassNames() {
        if (_entityClassNames == null)
            return Collections.emptyList();
        return _entityClassNames;
    }

    public void addManagedClassName(String name) {
        if (_entityClassNames == null)
            _entityClassNames = new ArrayList<>();
        _entityClassNames.add(name);
    }

    @Override
    public Properties getProperties() {
        Properties copy = new Properties();
        copy.putAll(_props);
        return copy;
    }

    public void setProperty(String key, String value) {
        _props.put(key, value);
    }

    @Override
    public void addTransformer(ClassTransformer transformer) {
        throw new UnsupportedOperationException();
    }

    /**
     * The location of the persistence.xml resource. May be null.
     */
    public URL getPersistenceXmlFileUrl() {
        return _persistenceXmlFile;
    }

    /**
     * The location of the persistence.xml resource. May be null.
     */
    public void setPersistenceXmlFileUrl(URL url) {
        _persistenceXmlFile = url;
    }

    /**
     * Load the given user-supplied map of properties into this persistence
     * unit.
     */
    public void fromUserProperties(Map map) {
        if (map == null)
            return;

        Object key;
        Object val;
        for (Object o : map.entrySet()) {
            key = ((Map.Entry) o).getKey();
            val = ((Map.Entry) o).getValue();
            if (JPAProperties.PROVIDER.equals(key))
                setPersistenceProviderClassName((String) val);
            else if (JPAProperties.TRANSACTION_TYPE.equals(key)) {
                setTransactionType(JPAProperties.getEnumValue(PersistenceUnitTransactionType.class, val));
            } else if (JPAProperties.DATASOURCE_JTA.equals(key)) {
                if (val instanceof String) {
                    setJtaDataSourceName((String) val);
                } else {
                    setJtaDataSource((DataSource) val);
                }
            } else if (JPAProperties.DATASOURCE_NONJTA.equals(key)) {
                if (val instanceof String) {
                    setNonJtaDataSourceName((String) val);
                } else {
                    setNonJtaDataSource((DataSource) val);
                }
            } else if (JPAProperties.VALIDATE_MODE.equals(key)) {
                setValidationMode(JPAProperties.getEnumValue(ValidationMode.class, val));
            } else if (JPAProperties.CACHE_MODE.equals(key)) {
                setSharedCacheMode(JPAProperties.getEnumValue(SharedCacheMode.class, val));
            } else {
                _props.put(key, val);
            }
        }
    }

    /**
     * Return a {@link Map} containing the properties necessary to create
     * a {@link Configuration} that reflects the information in this
     * persistence unit info.
     */
    public Map toOpenJPAProperties() {
        return toOpenJPAProperties(this);
    }

    /**
     * Return a {@link Map} containing the properties necessary to create
     * a {@link Configuration} that reflects the information in the given
     * persistence unit info.
     */
    public static Map toOpenJPAProperties(PersistenceUnitInfo info) {
        Map map = new HashMap<String,Object>();
        Set<String> added = new HashSet<>();
        if (info.getTransactionType() == PersistenceUnitTransactionType.JTA)
            replaceAsOpenJPAProperty(map, added, "TransactionMode", "managed");

        boolean hasJta = false;
        DataSource ds = info.getJtaDataSource();
        if (ds != null) {
            replaceAsOpenJPAProperty(map, added, "ConnectionFactory", ds);
            replaceAsOpenJPAProperty(map, added, "ConnectionFactoryMode", "managed");
            hasJta = true;
        } else if (info instanceof PersistenceUnitInfoImpl
            && ((PersistenceUnitInfoImpl) info).getJtaDataSourceName() != null){
            replaceAsOpenJPAProperty(map, added, "ConnectionFactoryName",
                    ((PersistenceUnitInfoImpl)info).getJtaDataSourceName());
            replaceAsOpenJPAProperty(map, added, "ConnectionFactoryMode", "managed");
            hasJta = true;
        }

        ds = info.getNonJtaDataSource();
        if (ds != null) {
             replaceAsOpenJPAProperty(map, added, hasJta ? "ConnectionFactory2" : "ConnectionFactory", ds);
        } else if (info instanceof PersistenceUnitInfoImpl
            && ((PersistenceUnitInfoImpl) info).getNonJtaDataSourceName() != null) {
            String nonJtaName = ((PersistenceUnitInfoImpl) info).getNonJtaDataSourceName();
            replaceAsOpenJPAProperty(map, added, hasJta ? "ConnectionFactory2Name" : "ConnectionFactoryName",
                    nonJtaName);
        }

        if (info.getClassLoader() != null)
            replaceAsOpenJPAProperty(map, added, "ClassResolver", new ClassResolverImpl(info.getClassLoader()));

        Properties props = info.getProperties();
        if (props != null) {
            // remove any of the things that were set above
            for (String key : added) {
                if (Configurations.containsProperty(key, props))
                    Configurations.removeProperty(key, props);
            }

            // add all the non-conflicting props in the <properties> section
            map.putAll(props);

            // this isn't a real config property; remove it
            map.remove(PersistenceProviderImpl.CLASS_TRANSFORMER_OPTIONS);
        }

        if (!Configurations.containsProperty("Id", map))
            map.put("openjpa.Id", info.getPersistenceUnitName());

        Properties metaFactoryProps = new Properties();
        if (info.getManagedClassNames() != null
            && !info.getManagedClassNames().isEmpty()) {
            StringBuilder types = new StringBuilder();
            for (String type : info.getManagedClassNames()) {
                if (types.length() > 0)
                    types.append(';');
                types.append(type);
            }
            metaFactoryProps.put("Types", types.toString());
        }
        if (info.getJarFileUrls() != null && !info.getJarFileUrls().isEmpty()
            || (!info.excludeUnlistedClasses()
            && info.getPersistenceUnitRootUrl() != null)) {
            StringBuilder jars = new StringBuilder();
            String file = null;
            if (!info.excludeUnlistedClasses()
                && info.getPersistenceUnitRootUrl() != null) {
                URL url = info.getPersistenceUnitRootUrl();
                if ("file".equals(url.getProtocol())) // exploded jar?
                    file = URLDecoder.decode(url.getPath());
                else
                    jars.append(url);
            }
            for (URL jar : info.getJarFileUrls()) {
                if (jars.length() > 0)
                    jars.append(';');
                jars.append(jar);
            }
            if (file != null)
                metaFactoryProps.put("Files", file);
            if (jars.length() != 0)
                metaFactoryProps.put("URLs", jars.toString());
        }
        if (info.getMappingFileNames() != null
            && !info.getMappingFileNames().isEmpty()) {
            StringBuilder rsrcs = new StringBuilder();
            for (String rsrc : info.getMappingFileNames()) {
                if (rsrcs.length() > 0)
                    rsrcs.append(';');
                rsrcs.append(rsrc);
            }
            metaFactoryProps.put("Resources", rsrcs.toString());
        }

        // set persistent class locations as properties of metadata factory,
        // combining them with any existing metadata factory props
        if (!metaFactoryProps.isEmpty()) {
            String key = ProductDerivations.getConfigurationKey
                ("MetaDataFactory", map);
            map.put(key, Configurations.combinePlugins((String) map.get(key),
                Configurations.serializeProperties(metaFactoryProps)));
        }

        // always record provider name for product derivations to access
        if (info.getPersistenceProviderClassName() != null)
            map.put(JPAProperties.PROVIDER, info.getPersistenceProviderClassName());

        // convert validation-mode enum to a StringValue
        if (info.getValidationMode() != null)
            map.put(JPAProperties.VALIDATE_MODE, info.getValidationMode());

        if (info.getPersistenceXMLSchemaVersion() != null) {
            map.put(PERSISTENCE_VERSION, info.getPersistenceXMLSchemaVersion());
        }

        if (info.getSharedCacheMode() != null) {
            map.put(JPAProperties.CACHE_MODE, info.getSharedCacheMode());
        }

        return map;
    }

    /**
     * Adds the given key-val to the given map after adding "openjpa." prefix to the key.
     * Tracks this addition in the given set of added keys.
     */
    private static void replaceAsOpenJPAProperty(Map map, Set<String> added, String key, Object val) {
        map.put("openjpa." + key, val);
        added.add(key);
    }

    // --------------------

    @Override
    public File getSourceFile() {
        if (_persistenceXmlFile == null)
            return null;

        try {
            return new File(_persistenceXmlFile.toURI());
        } catch (URISyntaxException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public Object getSourceScope() {
        return null;
    }

    @Override
    public int getSourceType() {
        return SRC_XML;
    }

    @Override
    public int getLineNumber() {
        return 0;
    }

    @Override
    public int getColNumber() {
        return 0;
    }

    @Override
    public String getResourceName() {
        return "PersistenceUnitInfo:" + _name;
    }

    /**
     * Simple class resolver built around the persistence unit loader.
     */
    public static class ClassResolverImpl
        implements ClassResolver {

        private final ClassLoader _loader;

        public ClassResolverImpl(ClassLoader loader) {
            _loader = loader;
        }

        @Override
        public ClassLoader getClassLoader(Class ctx, ClassLoader env) {
            return _loader;
        }
	}

    @Override
    public String getPersistenceXMLSchemaVersion() {
        return _schemaVersion;
    }

    public void setPersistenceXMLSchemaVersion(String version) {
        _schemaVersion = version;
    }

    @Override
    public ValidationMode getValidationMode() {
        return _validationMode;
    }

    public void setValidationMode(ValidationMode mode) {
        _validationMode = mode;
    }

    @Override
    public SharedCacheMode getSharedCacheMode() {
        return _sharedCacheMode;
    }

    public void setSharedCacheMode(SharedCacheMode mode) {
        _sharedCacheMode = mode;
    }
}
