/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.sessions.factories;

import java.io.File;
import java.net.URL;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;

import org.eclipse.persistence.exceptions.SessionLoaderException;
import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.internal.helper.ConversionManager;
import org.eclipse.persistence.internal.localization.ExceptionLocalization;
import org.eclipse.persistence.internal.sessions.factories.PersistenceEntityResolver;
import org.eclipse.persistence.internal.sessions.factories.SessionsFactory;
import org.eclipse.persistence.internal.sessions.factories.XMLSessionConfigProject_11_1_1;
import org.eclipse.persistence.internal.sessions.factories.XMLSessionConfigToplinkProject;
import org.eclipse.persistence.internal.sessions.factories.model.SessionConfigs;
import org.eclipse.persistence.oxm.XMLContext;
import org.eclipse.persistence.oxm.XMLUnmarshaller;
import org.eclipse.persistence.platform.xml.XMLParser;
import org.eclipse.persistence.platform.xml.XMLPlatform;
import org.eclipse.persistence.platform.xml.XMLPlatformFactory;
import org.eclipse.persistence.sessions.Project;
import org.eclipse.persistence.sessions.Session;
import org.w3c.dom.Document;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * Provide a mechanism for loading Session configuration XML files.
 * This is used by the SessionManager to define how to find and load a Session from a sessions XML file.
 * The sessions XML file is typically deployed in the applications jar (ejb-jar) and named sessions.xml in the /META-INF directory.
 * Several loading options are provided,
 * <ul>
 * <li> resourceName : The resource path and file name to the sessions XML file,
 * default is /sessions.xml or /META-INF/sessions.xml. (ensure "/" is used, not "\").
 * A file path can also be provided, although a resource is typically used.
 * <li> shouldLogin : Define if the loaded session should be connected, default true.
 * <li> shouldRefresh : Define if the loaded session should be refreshed from the file,
 * (this old session will be disconnected) default false.
 * <li> classLoader : Define the class loader that should be used to find the resource.
 * This loader will also be used as the loaded session's class loader.  This should be the application's class loader.
 * Default is the ConversionManager loader, which is thread-based.
 * <li> shouldCheckClassLoader : Defines if the session will be refreshed from the file if the class loader requesting the load,
 * is different than the loaded session's class loader.  This can be used to handle re-deployment.
 * </ul>
 *
 * @since TopLink 10.1.3
 * @author Guy Pelletier
 */
public class XMLSessionConfigLoader {
    protected String resourceName;

    /** Stores the resource path to provide a better error message if the load fails. */
    protected String resourcePath = DEFAULT_RESOURCE_NAME;
    /** Stores the name of the Session in the sessions XML file desired to be loaded. */
    protected String sessionName = "default";
    /** Define if the loaded session should be connected, default true. */
    protected boolean shouldLogin = true;
    /** Define if the loaded session should be refreshed from the file. */
    protected boolean shouldRefresh = false;
    /** Define the class loader that should be used to find the resource. */
    protected ClassLoader classLoader;
    /** Defines if the session will be refreshed from the file if the class loader requesting the load is different than the loaded session's class loader. */
    protected boolean shouldCheckClassLoader = false;
    /** Stores any exceptions that occurred to provide all the exceptions up front if the load fails. */
    protected Vector<Throwable> exceptionStore;
    /** Used to store the entity resolver to validate the XML schema when parsing. */
    protected PersistenceEntityResolver entityResolver;

    public static final String ECLIPSELINK_SESSIONS_SCHEMA = "org/eclipse/persistence/eclipselink_sessions_2.1.xsd";
    protected static final String DEFAULT_RESOURCE_NAME = "sessions.xml";
    protected static final String DEFAULT_RESOURCE_NAME_IN_META_INF = "META-INF/sessions.xml";

    /** Cache the creation and initialization of the Session XML mapping project. */
    protected static final Project project = new XMLSessionConfigProject_11_1_1();

    /** Cache the creation and initialization of the Session XML mapping project. */
    protected static Project getProject() {
        return project;
    }

    /**
     * PUBLIC:
     * This constructor is used when the file resource named 'sessions.xml' will
     * be parsed for configuration.
     */
    public XMLSessionConfigLoader() {
        this(DEFAULT_RESOURCE_NAME);
    }

    /**
     * PUBLIC:
     * This constructor is used when passing in the resource name of the
     * configuration file that should be parsed
     */
    public XMLSessionConfigLoader(String resourceName) {
        this.resourceName = resourceName;
        this.exceptionStore = new Vector<>();
        this.entityResolver = new PersistenceEntityResolver();
        this.classLoader = ConversionManager.getDefaultManager().getLoader();
    }

    /**
     * INTERNAL:
     * Will return the the resource name with full path of the resource file.
     */
    public String getResourcePath() {
        return this.resourcePath;
    }

    /**
     * INTERNAL:
     */
    public Vector<Throwable> getExceptionStore() {
        return this.exceptionStore;
    }

    /**
     * PUBLIC:
     * Returns the resource name we are trying to load.
     */
    public String getResourceName() {
        return this.resourceName;
    }

    /**
     * PUBLIC:
     * Set the resource name we are trying to load.
     */
    public void setResourceName(String resourceName) {
        this.resourceName = resourceName;
    }

    /**
     * PUBLIC:
     * Returns the name of the Session in the sessions XML file desired to be loaded.
     */
    public String getSessionName() {
        return this.sessionName;
    }

    /**
     * PUBLIC:
     * Set the name of the Session in the sessions XML file desired to be loaded.
     */
    public void setSessionName(String sessionName) {
        this.sessionName = sessionName;
    }

    /**
     * PUBLIC:
     * Return if the loaded session should be connected.
     */
    public boolean shouldLogin() {
        return shouldLogin;
    }

    /**
     * PUBLIC:
     * Set if the loaded session should be connected.
     */
    public void setShouldLogin(boolean shouldLogin) {
        this.shouldLogin = shouldLogin;
    }

    /**
     * PUBLIC:
     * Return if the loaded session should be refreshed from the file.
     */
    public boolean shouldRefresh() {
        return shouldRefresh;
    }

    /**
     * PUBLIC:
     * Set if the loaded session should be refreshed from the file.
     */
    public void setShouldRefresh(boolean shouldRefresh) {
        this.shouldRefresh = shouldRefresh;
    }

    /**
     * PUBLIC:
     * Return if the session will be refreshed from the file if the class loader requesting the load is different than the loaded session's class loader.
     */
    public boolean shouldCheckClassLoader() {
        return shouldCheckClassLoader;
    }

    /**
     * PUBLIC:
     * Set if the session will be refreshed from the file if the class loader requesting the load is different than the loaded session's class loader.
     */
    public void setShouldCheckClassLoader(boolean shouldCheckClassLoader) {
        this.shouldCheckClassLoader = shouldCheckClassLoader;
    }

    /**
     * PUBLIC:
     * Return the class loader that should be used to find the resource.
     */
    public ClassLoader getClassLoader() {
        return classLoader;
    }

    /**
     * PUBLIC:
     * Set the class loader that should be used to find the resource.
     */
    public void setClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    /**
     * INTERNAL:
     * This method instantiates the parser and builds the document based on the
     * schema. If the document is loaded without errors, then the configs are
     * converted to sessions and stored on the session manager and true is
     * returned to indicate success.
     */
    public boolean load(SessionManager sessionManager, ClassLoader loader) {
        Document document = loadDocument(loader);

        if(getExceptionStore().isEmpty()){
            if (document.getDocumentElement().getTagName().equals("sessions")) {
                return buildSessionConfigs(sessionManager,loader,document,getProject());
            }
        }else{
            //upon this time, we knew this could be either toplink sessions.xml or invalid eclipse session.xml.
            if(document.getDocumentElement().getTagName().equals("toplink-sessions")){
                return buildSessionConfigs(sessionManager,loader,document,new XMLSessionConfigToplinkProject());
            }else{
                // Throw the exceptions we encountered
                throw SessionLoaderException.finalException(getExceptionStore());
            }
        }
        // 9.0.4 session.xml, return false to indicate we should load with the XMLLoader
        return false;
    }

    private boolean buildSessionConfigs(SessionManager sessionManager, ClassLoader loader,Document document, Project project){
        // No errors occurred, unmasrshal the document which will return a
        // SessionConfigs containing 0 or more SessionConfigs and send
        // them through the factory to create actual Sessions
        XMLContext context = new XMLContext(project);
        XMLUnmarshaller unmarshaller = context.createUnmarshaller();
        SessionConfigs configs = (SessionConfigs)unmarshaller.unmarshal(document);
        SessionsFactory factory = new SessionsFactory();
        Map<String, Session> sessions = factory.buildSessionConfigs(configs, loader);
        for (Map.Entry<String, Session> entry: sessions.entrySet()) {
            // Only add the session if missing.
            if (!sessionManager.getSessions().containsKey(entry.getKey())) {
                sessionManager.addSession(entry.getKey(), entry.getValue());
            }
        }
        return true;
    }

    /**
     * INTERNAL:
     * This method is to be used to load config objects for the Mapping Workbench
     * only. Do not call this method.
     */
    public SessionConfigs loadConfigsForMappingWorkbench(ClassLoader loader) {
        return loadConfigsForMappingWorkbench(loader, true);
    }

    /**
     * INTERNAL:
     * This method is to be used to load config objects for the Mapping Workbench
     * only. Do not call this method.
     */
    public SessionConfigs loadConfigsForMappingWorkbench(ClassLoader loader, boolean validate) {
        Document document = loadDocument(loader, validate);

        if (getExceptionStore().isEmpty()) {
            if (document.getDocumentElement().getTagName().equals("sessions")) {
                // No errors occurred, unmarshal the document which will return a
                // SessionConfigs containing 0 or more SessionConfigs
                XMLContext context = new XMLContext(getProject());
                XMLUnmarshaller unmarshaller = context.createUnmarshaller();
                return (SessionConfigs)unmarshaller.unmarshal(document);
            }else{
                // 9.0.4 session.xml or invalid xml format.
                throw SessionLoaderException.InvalidSessionXML();
            }
        } else {
            if (document.getDocumentElement().getTagName().equals("toplink-sessions")) {
                // No errors occurred, unmarshal the document which will return a
                // SessionConfigs containing 0 or more SessionConfigs
                XMLContext context = new XMLContext(new XMLSessionConfigToplinkProject());
                XMLUnmarshaller unmarshaller = context.createUnmarshaller();
                return (SessionConfigs)unmarshaller.unmarshal(document);
            }else{
                // Throw the exceptions we encountered
                throw SessionLoaderException.finalException(getExceptionStore());
            }
        }
    }

    /**
     * INTERNAL:
     * Load a session.xml document. The error handler will capture all the
     * errors and allow for a document to be returned.
     */
    protected Document loadDocument(ClassLoader loader) {
        return loadDocument(loader, SessionManager.shouldUseSchemaValidation());
    }

    /**
     * INTERNAL:
     * Load a session.xml document. The error handler will capture all the
     * errors and allow for a document to be returned.
     */
    protected Document loadDocument(ClassLoader loader, boolean validate) {
        URL inURL = loader.getResource(this.resourceName);
        // Also allow a file reference.
        File inFile = new File(this.resourceName);

        if (inURL == null) {
            // If loading the default resource name and it is not found,
            // look in the META-INF directory.
            if (this.resourceName.equals(DEFAULT_RESOURCE_NAME)) {
                inURL = loader.getResource(DEFAULT_RESOURCE_NAME_IN_META_INF);
            }

            if ((inURL == null) && (!inFile.exists())) {
                throw ValidationException.noSessionsXMLFound(this.resourceName);
            }
        }

        if (inURL == null) {
            this.resourcePath = inFile.getAbsolutePath();
        } else {
            // Stored the loaded resource path, used in exception string if the
            // resource is not found or if there are errors in the resource.
            this.resourcePath = inURL.getPath();
        }

        XMLPlatform xmlPlatform = XMLPlatformFactory.getInstance().getXMLPlatform();
        XMLParser parser = xmlPlatform.newXMLParser();
        if (validate) {
            parser.setValidationMode(XMLParser.SCHEMA_VALIDATION);
        } else {
            parser.setValidationMode(XMLParser.NONVALIDATING);
        }

        parser.setWhitespacePreserving(false);
        parser.setXMLSchema(loader.getResource(ECLIPSELINK_SESSIONS_SCHEMA));
        parser.setEntityResolver(this.entityResolver);
        parser.setErrorHandler(new XMLSessionConfigLoaderErrorHandler());

        if (inURL == null) {
            return parser.parse(inFile);
        } else {
            return parser.parse(inURL);
        }
    }

    /**
     * INTERNAL:
     * <p><b>Purpose</b>: Provide a mechanism to swallow all parsing errors
     *
     * @see ErrorHandler
     * @since TopLink 10.1.3
     * @author Guy Pelletier
     */
    public class XMLSessionConfigLoaderErrorHandler implements ErrorHandler {
        public void warning(SAXParseException e) throws SAXException {
            getExceptionStore().add(SessionLoaderException.failedToParseXML(ExceptionLocalization.buildMessage("parsing_warning"), e.getLineNumber(), e.getColumnNumber(), e));
        }

        public void error(SAXParseException e) throws SAXException {
            getExceptionStore().add(e);
        }

        public void fatalError(SAXParseException e) throws SAXException {
            getExceptionStore().add(e);
        }
    }
}
