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

// javase imports
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import org.w3c.dom.Document;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

// EclipseLink imports
import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.exceptions.XMLMarshalException;
import org.eclipse.persistence.internal.helper.ConversionManager;
import org.eclipse.persistence.internal.sessions.factories.EclipseLinkObjectPersistenceRuntimeXMLProject;
import org.eclipse.persistence.internal.sessions.factories.MissingDescriptorListener;
import org.eclipse.persistence.internal.sessions.factories.ObjectPersistenceRuntimeXMLProject;
import org.eclipse.persistence.internal.sessions.factories.ObjectPersistenceRuntimeXMLProject_11_1_1;
import org.eclipse.persistence.oxm.XMLContext;
import org.eclipse.persistence.oxm.XMLLogin;
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;

/**
 * <p><b>Purpose</b>: Allow for a EclipseLink Mapping Workbench generated deployment XML project file to be read.
 * This reader returns an instance of Project to be used to initialize a EclipseLink session.
 * This class supports reading the 11g 11.1.1 and 10g 10.1.3.
 *
 * @since TopLink 3.0
 * @author James Sutherland
 */
public class XMLProjectReader {

    /** Allow for usage of schema validation to be configurable. */
    protected static boolean shouldUseSchemaValidation = false; // switch back for 1.0

    /** Cache the creation and initialization of the EclipseLink XML mapping project. */
    protected static Project project;
    public static final String SCHEMA_DIR = "org/eclipse/persistence/";
    public static final String OPM_SCHEMA = "object-persistence_1_0.xsd";
    public static final String ECLIPSELINK_SCHEMA = "eclipselink_persistence_map_2.3.xsd";
    public static final String ECLIPSELINK_1_0_SCHEMA = "eclipselink_persistence_map_1.0.xsd";
    public static final String TOPLINK_11_SCHEMA = "toplink-object-persistence_11_1_1.xsd";
    public static final String TOPLINK_10_SCHEMA = "toplink-object-persistence_10_1_3.xsd";

    /**
     * PUBLIC:
     * Return if schema validation will be used when parsing the deployment XML.
     */
    public static boolean shouldUseSchemaValidation() {
        return shouldUseSchemaValidation;
    }

    /**
     * PUBLIC:
     * Set if schema validation will be used when parsing the deployment XML.
     * By default schema validation is on, but can be turned off if validation problems occur,
     * or to improve parsing performance.
     */
    public static void setShouldUseSchemaValidation(boolean value) {
        shouldUseSchemaValidation = value;
    }

    public XMLProjectReader() {
        super();
    }

    /**
     * PUBLIC:
     * Read the EclipseLink project deployment XML from the file or resource name.
     * If a resource name is used the default class loader will be used to resolve the resource.
     * Note the default class loader must be able to resolve the domain classes.
     * Note the file must be the deployment XML, not the Mapping Workbench project file.
     */
    public static Project read(String fileOrResourceName) {
        return read(fileOrResourceName, null);
    }


    /**
     * PUBLIC:
     * Read the EclipseLink project deployment XML from the reader on the file.
     * Note the class loader must be able to resolve the domain classes.
     * Note the file must be the deployment XML, not the Mapping Workbench project file.
     * This API supports 10g (10.0.3), 11g (11.1.1) formats.
     */
    public static Project read(Reader reader, ClassLoader classLoader) {
        // Since a reader is pass and it can only be streamed once (mark does not work)
        // It must be first read into a buffer so multiple readers can be used to
        // determine the format.  This does not effect performance severely.
        StringWriter writer;
        Document document;
        try {
            writer = new StringWriter(4096);
            char[] c = new char[4096];
            int r = 0;
            while ((r = reader.read(c)) != -1) {
                writer.write(c, 0, r);
            }
            String schema = null;
            if (shouldUseSchemaValidation()) {
                schema = SCHEMA_DIR + ECLIPSELINK_SCHEMA;
            }
            // Assume the format is OPM parse the document with OPM validation on.
            XMLPlatform xmlPlatform = XMLPlatformFactory.getInstance().getXMLPlatform();
            XMLParser parser = createXMLParser(xmlPlatform, true, false, schema);

            try {
                document = parser.parse(new StringReader(writer.toString()));
            } catch (Exception parseException) {
                // If the parse fails, it may be because the format was EclipseLink 1.0
                try {
                    if (shouldUseSchemaValidation()) {
                        schema = SCHEMA_DIR + ECLIPSELINK_1_0_SCHEMA;
                    }
                    parser = createXMLParser(xmlPlatform, true, false, schema);
                    document = parser.parse(new StringReader(writer.toString()));
                } catch (Exception parseException2){
                    // If the parse fails, it may be because the format was 11.1.1
                    try {
                        if (shouldUseSchemaValidation()) {
                            schema = SCHEMA_DIR + TOPLINK_11_SCHEMA;
                        }
                        parser = createXMLParser(xmlPlatform, true, false, schema);
                        document = parser.parse(new StringReader(writer.toString()));
                    } catch (Exception parseException3){
                        // If the parse validation fails, it may be because the format was 904 which is
                        // not support in eclipselink, just not valid, through original exception.
                        throw parseException;
                    }

                    String version = document.getDocumentElement().getAttribute("version");
                    // If 10.1.3 format use old format read.
                    if ((version == null) || (version.indexOf("1.0") == -1)) {
                        throw parseException;
                    }
                }
            }
        } catch (Exception exception) {
            throw XMLMarshalException.unmarshalException(exception);
        }

        String version = document.getDocumentElement().getAttribute("version");
        // If 10.1.3 format use old format read.
        if (version != null) {
            if (version.indexOf("10.1.3") != -1) {
                return read1013Format(document, classLoader);
            } else if (version.indexOf("11.1.1") != -1) {
                   return read1111Format(document, classLoader);
            }
            if (version.indexOf("TopLink") != -1) {
                //default to read 11.1.1
                return read1111Format(document, classLoader);
            }
        }


        if (project == null) {
            project = new EclipseLinkObjectPersistenceRuntimeXMLProject();
        }
        // bug261072: clone the project since readObjectPersistenceRuntimeFormat will change its datasourceLogin and Classloader
        return readObjectPersistenceRuntimeFormat(document, classLoader, project.clone());
    }

    private static XMLParser createXMLParser(XMLPlatform xmlPlatform, boolean namespaceAware, boolean whitespacePreserving, String schema){
        XMLParser parser = xmlPlatform.newXMLParser();
        parser.setNamespaceAware(namespaceAware);
        parser.setWhitespacePreserving(whitespacePreserving);
        if (schema != null) {
            parser.setValidationMode(XMLParser.SCHEMA_VALIDATION);
            // Workaround for bug #3503583.
            XMLSchemaResolver xmlSchemaResolver = new XMLSchemaResolver();
            URL eclipselinkSchemaURL = xmlSchemaResolver.resolveURL(schema);
            parser.setEntityResolver(xmlSchemaResolver);
            parser.setXMLSchema(eclipselinkSchemaURL);
        }
        return parser;
    }

    /**
     * PUBLIC:
     * Read the EclipseLink project deployment XML from the file or resource name.
     * If a resource name is used the class loader will be used to resolve the resource.
     * Note the class loader must be able to resolve the domain classes.
     * Note the file must be the deployment XML, not the Mapping Workbench project file.
     */
    public static Project read(String fileOrResourceName, ClassLoader classLoader) {
        if (fileOrResourceName.toLowerCase().indexOf(".mwp") != -1) {
            throw ValidationException.invalidFileName(fileOrResourceName);
        }
        InputStream fileStream = null;
        if (classLoader == null) {
            fileStream = (new ConversionManager()).getLoader().getResourceAsStream(fileOrResourceName);
        } else {
            fileStream = classLoader.getResourceAsStream(fileOrResourceName);
        }
        if (fileStream == null) {
            File file = new File(fileOrResourceName);
            if (!file.exists()) {
                throw ValidationException.projectXMLNotFound(fileOrResourceName, null);
            }
            try {
                fileStream = new FileInputStream(fileOrResourceName);
            } catch (FileNotFoundException exception) {
                throw ValidationException.projectXMLNotFound(fileOrResourceName, exception);
            }
        }

        InputStreamReader reader = null;
        try {
            try {
                // Bug2631348  Only UTF-8 is supported
                reader = new InputStreamReader(fileStream, "UTF-8");
            } catch (UnsupportedEncodingException exception) {
                throw ValidationException.fatalErrorOccurred(exception);
            }

            Project project = read(reader, classLoader);
            return project;
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (IOException exception) {
                throw ValidationException.fileError(exception);
            }
        }
    }

    /**
     * INTERNAL:
     * Read the TopLink 10.1.3 deployment XML format.
     */
    public static Project read1013Format(Document document, ClassLoader classLoader) {
        Project opmProject = new ObjectPersistenceRuntimeXMLProject();
        return readObjectPersistenceRuntimeFormat(document, classLoader, opmProject);
    }
    /**
     * INTERNAL:
     * Read the TopLink 11.1.1 deployment XML format.
     */
    public static Project read1111Format(Document document, ClassLoader classLoader) {
        Project opmProject = new ObjectPersistenceRuntimeXMLProject_11_1_1();
        return readObjectPersistenceRuntimeFormat(document, classLoader, opmProject);
    }

    /**
     * Read a project in the format of an ObjectPersistenceRuntimeXMLProject.
     * This could include a TopLink 11.1.1 project or a TopLink 10.1.3 project
     * @param document
     * @param classLoader
     * @param opmProject
     * @return
     */
    public static Project readObjectPersistenceRuntimeFormat(Document document, ClassLoader classLoader, Project opmProject){
        XMLLogin xmlLogin = new XMLLogin();
        xmlLogin.setDatasourcePlatform(new org.eclipse.persistence.oxm.platform.DOMPlatform());
        opmProject.setDatasourceLogin(xmlLogin);

        // Create the OPM project.
        if (classLoader != null) {
            xmlLogin.getDatasourcePlatform().getConversionManager().setLoader(classLoader);
        }
        // Marshal OPM format.
        XMLContext context = new XMLContext(opmProject);
        context.getSession(Project.class).getEventManager().addListener(new MissingDescriptorListener());
        XMLUnmarshaller unmarshaller = context.createUnmarshaller();
        Project project = (Project)unmarshaller.unmarshal(document);

        // Set the project's class loader.
        if ((classLoader != null) && (project.getDatasourceLogin() != null)) {
            project.getDatasourceLogin().getDatasourcePlatform().getConversionManager().setLoader(classLoader);
        }
        return project;
    }

    /**
     * PUBLIC:
     * Read the EclipseLink project deployment XML from the reader on the file.
     * Note the default class loader must be able to resolve the domain classes.
     * Note the file must be the deployment XML, not the Mapping Workbench project file.
     */
    public static Project read(Reader reader) {
        return read(reader, null);
    }

    /**
     * INTERNAL:
     * Workaround for bug #3503583.
     * This works around a bug in the xdk in resolving relative jar based xsd references in oc4j.
     */
    private static class XMLSchemaResolver implements EntityResolver {

        /**
         * INTERNAL:
         */
        public XMLSchemaResolver() {
            super();
        }

        /**
         * INTERNAL:
         * Resolve the XSD.
         */
        public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
            if (OPM_SCHEMA.equals(systemId)) {
                URL url = resolveURL(SCHEMA_DIR + OPM_SCHEMA);
                if (null == url) {
                    return null;
                }
                return new InputSource(url.openStream());
            }
            return null;
        }

        /**
         * INTERNAL:
         * Return the URL for the resource.
         */
        public URL resolveURL(String resource) {
            // The xsd is always in the eclipselink.jar, use our class loader.
            return getClass().getClassLoader().getResource(resource);
        }
    }
}
