/*
 * Copyright (c) 1998, 2021 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:
// dmccann - June 17/2009 - 2.0 - Initial implementation
package org.eclipse.persistence.testing.jaxb.externalizedmetadata;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;

import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.SchemaOutputResolver;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.eclipse.persistence.core.sessions.CoreProject;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.internal.helper.ConversionManager;
import org.eclipse.persistence.internal.jaxb.JaxbClassLoader;
import org.eclipse.persistence.jaxb.JAXBContext;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.JAXBContextProperties;
import org.eclipse.persistence.jaxb.compiler.Generator;
import org.eclipse.persistence.jaxb.javamodel.reflection.JavaModelImpl;
import org.eclipse.persistence.jaxb.javamodel.reflection.JavaModelInputImpl;
import org.eclipse.persistence.oxm.XMLContext;
import org.eclipse.persistence.platform.xml.XMLComparer;
import org.eclipse.persistence.sessions.Project;
import org.eclipse.persistence.testing.jaxb.JAXBXMLComparer;
import org.eclipse.persistence.testing.oxm.OXTestCase;
import org.w3c.dom.Document;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSResourceResolver;
import org.xml.sax.InputSource;

import junit.framework.TestCase;

/**
 * Tests Externalized Metadata functionality.
 *
 */
public class ExternalizedMetadataTestCases extends TestCase {
    private static final String tmpdir = System.getenv("T_WORK") == null
            ? System.getProperty("java.io.tmpdir") : System.getenv("T_WORK");
    protected static ClassLoader loader = Thread.currentThread().getContextClassLoader();
    protected static String EMPTY_NAMESPACE = "";
    protected JAXBContext jaxbContext;
    protected DocumentBuilder parser;

    /**
     * This is the preferred (and only) constructor.
     *
     */
    public ExternalizedMetadataTestCases(String name) {
        super(name);
    }

    @Override
    public void setUp() throws Exception {
        DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
        builderFactory.setNamespaceAware(true);
        builderFactory.setIgnoringElementContentWhitespace(true);
        parser = builderFactory.newDocumentBuilder();
    }

    /**
     * Convenience method that will return the JAXBContext instance created during
     * the last generateSchema call.
     *
     * Assumes a prior call to generateSchema has been made.
     *
     */
    public JAXBContext getJAXBContext() {
        return jaxbContext;
    }

    /**
     * Generate the schema(s) for a given set of classes.  Any eclipselink-oxm.xml file(s)
     * found in the package(s) will be applied by default.
     *
     */
    public MySchemaOutputResolver generateSchema(Class<?>[] classes, int expectedSchemaCount) {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        MySchemaOutputResolver outputResolver = new MySchemaOutputResolver();
        try {
            generateSchema(classes, null, outputResolver, classLoader);
        } catch (Exception ex) {
            ex.printStackTrace();
            fail("Schema generation failed unexpectedly: " + ex.toString());
        }
        assertTrue("No schemas were generated", outputResolver.schemaFiles.size() > 0);
        assertTrue("Expected schema generation count to be ["+expectedSchemaCount+"], but was [" + outputResolver.schemaFiles.size() + "]", outputResolver.schemaFiles.size() == expectedSchemaCount);
        return outputResolver;
    }

    /**
     * Generate the schema using the CONTEXT_PATH.
     *
     */
    public MySchemaOutputResolver generateSchema(String contextPath, int expectedSchemaCount) {
        MySchemaOutputResolver outputResolver = new MySchemaOutputResolver();
        try {
            generateSchema(contextPath, outputResolver);
        } catch (Exception ex) {
            ex.printStackTrace();
            fail("Schema generation failed unexpectedly: " + ex.toString());
        }
        assertTrue("No schemas were generated", outputResolver.schemaFiles.size() > 0);
        assertTrue("Expected schema generation count to be ["+expectedSchemaCount+"], but was [" + outputResolver.schemaFiles.size() + "]", outputResolver.schemaFiles.size() == expectedSchemaCount);
        return outputResolver;
    }

    /**
     * Generate the schema using the CONTEXT_PATH.
     *
     */
    public void generateSchema(String contextPath, int expectedSchemaCount, MyStreamSchemaOutputResolver outputResolver) {
        try {
            generateSchema(contextPath, outputResolver);
        } catch (Exception ex) {
            ex.printStackTrace();
            fail("Schema generation failed unexpectedly: " + ex.toString());
        }
        assertTrue("No schemas were generated", outputResolver.schemaFiles.size() > 0);
        assertTrue("Expected schema generation count to be ["+expectedSchemaCount+"], but was [" + outputResolver.schemaFiles.size() + "]", outputResolver.schemaFiles.size() == expectedSchemaCount);
    }

    /**
     * Generate the schema(s) for a given context path, and apply the eclipselink-oxm.xml
     * file(s) in the properties map.
     *
     */
    public MySchemaOutputResolver generateSchema(String contextPath, Map<String, Object> properties, int expectedSchemaCount) {
        MySchemaOutputResolver outputResolver = new MySchemaOutputResolver();
        try {
            generateSchema(contextPath, outputResolver, properties);
        } catch (Exception ex) {
            ex.printStackTrace();
            fail("Schema generation failed unexpectedly: " + ex.toString());
        }
        assertTrue("No schemas were generated", outputResolver.schemaFiles.size() > 0);
        assertTrue("Expected schema generation count to be ["+expectedSchemaCount+"], but was [" + outputResolver.schemaFiles.size() + "]", outputResolver.schemaFiles.size() == expectedSchemaCount);
        return outputResolver;
    }

    /**
     * Generate the schema(s) for a given set of classes, and apply the eclipselink-oxm.xml
     * file found on the path.  The eclipselink-oxm.xml will be stored in the property map
     * using the contextPath as a key (maps package name to xml metadata file).
     *
     * @param contextPath used as key for storing eclipselink-oxm.xml file Source in properties map
     * @param iStream eclipselink-oxm.xml file as a stream
     */
    private MySchemaOutputResolver generateSchema(String contextPath, InputStream iStream, int expectedSchemaCount) {
        HashMap<String, Source> metadataSourceMap = new HashMap<>();
        metadataSourceMap.put(contextPath, new StreamSource(iStream));
        validateBindingsFileAgainstSchema(iStream);
        Map<String, Object> properties = new HashMap<>();
        properties.put(JAXBContextProperties.OXM_METADATA_SOURCE, metadataSourceMap);
        return generateSchema(contextPath, properties, expectedSchemaCount);
    }

    /**
     * Generate the schema(s) for a given set of classes, and apply the eclipselink-oxm.xml
     * file found on the path.  The eclipselink-oxm.xml will be stored in the property map
     * using the contextPath as a key (maps package name to xml metadata file).
     *
     * @param contextPath used as key for storing eclipselink-oxm.xml file Source in properties map
     * @param path eclipselink-oxm.xml file will be searched for on this path
     */
    public MySchemaOutputResolver generateSchema(String contextPath, String path, int expectedSchemaCount) {
        String metadataFile = path + "eclipselink-oxm.xml";
        return generateSchemaWithFileName(contextPath, metadataFile, expectedSchemaCount);
    }

    public MySchemaOutputResolver generateSchemaWithFileName(String contextPath, String metadataFile, int expectedSchemaCount) {
        InputStream iStream = loader.getResourceAsStream(metadataFile);
        InputStream iStreamCopy = loader.getResourceAsStream(metadataFile);
        if (iStream == null) {
            fail("Couldn't load metadata file [" + metadataFile + "]");
        }
        validateBindingsFileAgainstSchema(iStreamCopy);
        HashMap<String, Source> metadataSourceMap = new HashMap<>();
        metadataSourceMap.put(contextPath, new StreamSource(iStream));
        Map<String, Object> properties = new HashMap<>();
        properties.put(JAXBContextProperties.OXM_METADATA_SOURCE, metadataSourceMap);
        return generateSchema(contextPath, properties, expectedSchemaCount);
    }

    /**
     * Generate the schema(s) for a given set of classes, and apply the eclipselink-oxm.xml
     * file found on the path.  The eclipselink-oxm.xml will be stored in the property map
     * using the contextPath as a key (maps package name to xml metadata file).
     *
     * @param contextPath used as key for storing eclipselink-oxm.xml file Source in properties map
     * @param iStream eclipselink-oxm.xml file as a stream
     */
    private MySchemaOutputResolver generateSchema(Class<?>[] classes, String contextPath, InputStream iStream, int expectedSchemaCount) {
        HashMap<String, Source> metadataSourceMap = new HashMap<String, Source>();
        metadataSourceMap.put(contextPath, new StreamSource(iStream));
        Map<String, Map<String, Source>> properties = new HashMap<String, Map<String, Source>>();
        properties.put(JAXBContextProperties.OXM_METADATA_SOURCE, metadataSourceMap);
        MySchemaOutputResolver outputResolver = new MySchemaOutputResolver();
        try {
            generateSchema(classes, properties, outputResolver, loader);
        } catch (Exception ex) {
            ex.printStackTrace();
            fail("Schema generation failed unexpectedly: " + ex.toString());
        }
        assertTrue("No schemas were generated", outputResolver.schemaFiles.size() > 0);
        assertTrue("Expected schema generation count to be ["+expectedSchemaCount+"], but was [" + outputResolver.schemaFiles.size() + "]", outputResolver.schemaFiles.size() == expectedSchemaCount);
        return outputResolver;
    }

    /**
     * Generate the schema(s) for a given set of classes, and apply the eclipselink-oxm.xml
     * file found on the path.  The eclipselink-oxm.xml will be stored in the property map
     * using the contextPath as a key (maps package name to xml metadata file).
     *
     * @param contextPath used as key for storing eclipselink-oxm.xml file Source in properties map
     * @param path eclipselink-oxm.xml file will be searched for on this path
     */
    public MySchemaOutputResolver generateSchema(Class<?>[] classes, String contextPath, String path, int expectedSchemaCount) {
        String metadataFile = path + "eclipselink-oxm.xml";
        return generateSchemaWithFileName(classes, contextPath, metadataFile, expectedSchemaCount);
    }

    public void generateSchemaWithFileName(Class<?>[] classes, String contextPath, String metadataFile, int expectedSchemaCount, MyStreamSchemaOutputResolver outputResolver) {
        InputStream iStream = loader.getResourceAsStream(metadataFile);
        InputStream iStreamCopy = loader.getResourceAsStream(metadataFile);
        if (iStream == null) {
            fail("Couldn't load metadata file [" + metadataFile + "]");
        }
        HashMap<String, Source> metadataSourceMap = new HashMap<String, Source>();
        metadataSourceMap.put(contextPath, new StreamSource(iStream));
        Map<String, Map<String, Source>> properties = new HashMap<String, Map<String, Source>>();
        properties.put(JAXBContextProperties.OXM_METADATA_SOURCE, metadataSourceMap);

        validateBindingsFileAgainstSchema(iStreamCopy);

        try {
            generateSchema(classes, properties, outputResolver, loader);
        } catch (Exception ex) {
            ex.printStackTrace();
            fail("Schema generation failed unexpectedly: " + ex.toString());
        }
        if(expectedSchemaCount >0){
            assertTrue("No schemas were generated", outputResolver.schemaFiles.size() > 0);
        }
        assertTrue("Expected schema generation count to be ["+expectedSchemaCount+"], but was [" + outputResolver.schemaFiles.size() + "]", outputResolver.schemaFiles.size() == expectedSchemaCount);
    }

    public MySchemaOutputResolver generateSchemaWithFileName(Class<?>[] classes, String contextPath, String metadataFile, int expectedSchemaCount) {
        InputStream iStream = loader.getResourceAsStream(metadataFile);
        InputStream iStreamCopy = loader.getResourceAsStream(metadataFile);
        if (iStream == null) {
            fail("Couldn't load metadata file [" + metadataFile + "]");
        }
        HashMap<String, Source> metadataSourceMap = new HashMap<String, Source>();
        metadataSourceMap.put(contextPath, new StreamSource(iStream));
        Map<String, Map<String, Source>> properties = new HashMap<String, Map<String, Source>>();
        properties.put(JAXBContextProperties.OXM_METADATA_SOURCE, metadataSourceMap);
        MySchemaOutputResolver outputResolver = new MySchemaOutputResolver();

        validateBindingsFileAgainstSchema(iStreamCopy);

        try {
            generateSchema(classes, properties, outputResolver, loader);
        } catch (Exception ex) {
            ex.printStackTrace();
            fail("Schema generation failed unexpectedly: " + ex.toString());
        }
        if(expectedSchemaCount >0){
            assertTrue("No schemas were generated", outputResolver.schemaFiles.size() > 0);
        }
        assertTrue("Expected schema generation count to be ["+expectedSchemaCount+"], but was [" + outputResolver.schemaFiles.size() + "]", outputResolver.schemaFiles.size() == expectedSchemaCount);
        return outputResolver;
    }

    /**
     * Generate the schema(s) for a given set of types, and apply the eclipselink-oxm.xml
     * file found on the path.  The eclipselink-oxm.xml will be stored in the property map
     * using the contextPath as a key (maps package name to xml metadata file).
     *
     * @param contextPath used as key for storing eclipselink-oxm.xml file Source in properties map
     * @param path eclipselink-oxm.xml file will be searched for on this path
     */
    public MySchemaOutputResolver generateSchema(Type[] types, String contextPath, String path, int expectedSchemaCount) {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        String metadataFile = path + "eclipselink-oxm.xml";

        InputStream iStream = classLoader.getResourceAsStream(metadataFile);
        InputStream iStreamCopy = classLoader.getResourceAsStream(metadataFile);
        if (iStream == null) {
            fail("Couldn't load metadata file [" + metadataFile + "]");
        }
        validateBindingsFileAgainstSchema(iStreamCopy);

        HashMap<String, Source> metadataSourceMap = new HashMap<String, Source>();
        metadataSourceMap.put(contextPath, new StreamSource(iStream));
        Map<String, Map<String, Source>> properties = new HashMap<String, Map<String, Source>>();
        properties.put(JAXBContextProperties.OXM_METADATA_SOURCE, metadataSourceMap);
        MySchemaOutputResolver outputResolver = new MySchemaOutputResolver();
        try {
            generateSchema(types, properties, outputResolver, classLoader);
        } catch (Exception ex) {
            ex.printStackTrace();
            fail("Schema generation failed unexpectedly: " + ex.toString());
        }
        assertTrue("No schemas were generated", outputResolver.schemaFiles.size() > 0);
        assertTrue("Expected schema generation count to be ["+expectedSchemaCount+"], but was [" + outputResolver.schemaFiles.size() + "]", outputResolver.schemaFiles.size() == expectedSchemaCount);
        return outputResolver;
    }

    /**
     * Generate one or more schemas from a context path.
     *
     */
    protected void generateSchema(String contextPath, SchemaOutputResolver outputResolver, Map<String, Object> properties) throws Exception {
        try {
            jaxbContext = (JAXBContext) JAXBContextFactory.createContext(contextPath, loader, properties);
            jaxbContext.generateSchema(outputResolver);
        } catch (JAXBException e) {
            e.printStackTrace();
        }
    }

    /**
     * Generate one or more schemas from a context path.
     *
     */
    protected void generateSchema(String contextPath, SchemaOutputResolver outputResolver) throws Exception {
        try {
            jaxbContext = (JAXBContext) JAXBContextFactory.createContext(contextPath, loader);
            jaxbContext.generateSchema(outputResolver);
        } catch (JAXBException e) {
            e.printStackTrace();
        }
    }

    /**
     * Generate one or more schemas from an array of classes and a Map containing zero or more
     * eclipselink-oxm.xml entries.
     *
     */
    protected void generateSchema(Class<?>[] classesToBeBound, java.util.Map properties, SchemaOutputResolver outputResolver, ClassLoader classLoader) throws Exception {
        try {
            jaxbContext = (JAXBContext) JAXBContextFactory.createContext(classesToBeBound, properties, classLoader);
            jaxbContext.generateSchema(outputResolver);
        } catch (JAXBException e) {
            e.printStackTrace();
        }
    }

    /**
     * Generate one or more schemas from an array of types and a Map containing zero or more
     * eclipselink-oxm.xml entries.
     *
     */
    protected void generateSchema(Type[] typesToBeBound, java.util.Map properties, SchemaOutputResolver outputResolver, ClassLoader classLoader) throws Exception {
        try {
            jaxbContext = (JAXBContext) JAXBContextFactory.createContext(typesToBeBound, properties, classLoader);
            jaxbContext.generateSchema(outputResolver);
        } catch (JAXBException e) {
            e.printStackTrace();
        }
    }

    /**
     * Validates a given instance doc against the generated schema.
     *
     * @param namespace index in output resolver's list of generated schemas
     * @param outputResolver contains one or more schemas to validate against
     */
    protected String validateAgainstSchema(String src, String namespace, MySchemaOutputResolver outputResolver) {
        SchemaFactory sFact = SchemaFactory.newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI);
        Schema theSchema;
        try {
            theSchema = sFact.newSchema(outputResolver.schemaFiles.get(namespace));
            Validator validator = theSchema.newValidator();
            StreamSource ss = new StreamSource(new File(src));
            validator.validate(ss);
        } catch (Exception e) {
            //e.printStackTrace();
            if (e.getMessage() == null) {
                return "An unknown exception occurred.";
            }
            return e.getMessage();
        }
        return null;
    }

    /**
     * Validates a given instance doc against the generated schema.
     *
     * @param namespace index in output resolver's list of generated schemas
     * @param outputResolver contains one or more schemas to validate against
     */
    protected String validateAgainstSchema(InputStream src, String namespace, MySchemaOutputResolver outputResolver) {
        SchemaFactory sFact = SchemaFactory.newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI);
        Schema theSchema;
        try {
            theSchema = sFact.newSchema(outputResolver.schemaFiles.get(namespace));
            Validator validator = theSchema.newValidator();
            StreamSource ss = new StreamSource(src);
            validator.validate(ss);
        } catch (Exception e) {
            //e.printStackTrace();
            if (e.getMessage() == null) {
                return "An unknown exception occurred.";
            }
            return e.getMessage();
        }
        return null;
    }

    /**
     * Validates a given instance doc against a given schema.
     *
     */
    protected String validateAgainstSchema(String src, String schema) {
        SchemaFactory sFact = SchemaFactory.newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI);
        Schema theSchema;
        try {
            theSchema = sFact.newSchema(new File(schema));
            Validator validator = theSchema.newValidator();
            StreamSource ss = new StreamSource(new File(src));
            validator.validate(ss);
        } catch (Exception e) {
            //e.printStackTrace();
            if (e.getMessage() == null) {
                return "An unknown exception occurred.";
            }
            return e.getMessage();
        }
        return null;
    }

    /**
     * Validates a given instance doc against the generated schema.
     *
     * @param namespace index in output resolver's list of generated schemas
     * @param outputResolver contains one or more schemas to validate against
     */
    protected String validateAgainstSchema(String src, String namespace, MyStreamSchemaOutputResolver outputResolver) {
        SchemaFactory sFact = SchemaFactory.newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI);
        sFact.setResourceResolver(new ResourceResolver(outputResolver));
        Schema theSchema;
        try {
            Writer sw = outputResolver.schemaFiles.get(namespace);
            theSchema = sFact.newSchema(new StreamSource(new StringReader(sw.toString())));
            Validator validator = theSchema.newValidator();
            StreamSource ss = new StreamSource(new File(src));
            validator.validate(ss);
        } catch (Exception e) {
            //e.printStackTrace();
            if (e.getMessage() == null) {
                return "An unknown exception occurred.";
            }
            return e.getMessage();
        }
        return null;
    }

    /**
     * Class responsible from resolving schema imports during schema
     * validation.
     *
     */
    class ResourceResolver implements LSResourceResolver {
        private MyStreamSchemaOutputResolver oResolver;

        public ResourceResolver(MyStreamSchemaOutputResolver resolver) {
            oResolver = resolver;
        }
        @Override
        public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId, String baseUri) {
            return new MyLSInput(namespaceURI, oResolver);
        }
    }

    /**
     * Class which will be returned to from resolveResource() call in
     * ResourceResolver.
     *
     */
    class MyLSInput implements LSInput {
        private String sValue;
        private MyStreamSchemaOutputResolver oResolver;

        public MyLSInput(String value, MyStreamSchemaOutputResolver resolver) {
            sValue = value;
            oResolver = resolver;
        }
        @Override
        public void setSystemId(String arg0) {}
        @Override
        public void setStringData(String arg0) {}
        @Override
        public void setPublicId(String arg0) {}
        @Override
        public void setEncoding(String arg0) {}
        @Override
        public void setCharacterStream(Reader arg0) {}
        @Override
        public void setCertifiedText(boolean arg0) {}
        @Override
        public void setByteStream(InputStream arg0) {}
        @Override
        public void setBaseURI(String arg0) {}
        @Override
        public String getSystemId() {return null;}
        @Override
        public String getStringData() {
            return oResolver.schemaFiles.get(sValue).toString();
        }
        @Override
        public String getPublicId() {return null;}
        @Override
        public String getEncoding() {return null;}
        @Override
        public Reader getCharacterStream() {return null;}
        @Override
        public boolean getCertifiedText() {return false;}
        @Override
        public InputStream getByteStream() {return null;}
        @Override
        public String getBaseURI() {return null;}
    }

    /**
     * Validates a given bindings file against the eclipselink oxm schema.
     *
     */
    protected void validateBindingsFileAgainstSchema(InputStream src) {
        String result = null;
        SchemaFactory sFact = SchemaFactory.newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI);
        Schema theSchema;
        try {
            InputStream bindingsFileXSDInputStream = getClass().getClassLoader().getResourceAsStream("eclipselink_oxm_2_6.xsd");
            if (bindingsFileXSDInputStream == null){
                bindingsFileXSDInputStream = getClass().getClassLoader().getResourceAsStream("org/eclipse/persistence/jaxb/eclipselink_oxm_2_6.xsd");
            }
            if (bindingsFileXSDInputStream == null){
                fail("ERROR LOADING eclipselink_oxm_2_6.xsd");
            }
            Source bindingsFileXSDSource = new StreamSource(bindingsFileXSDInputStream);
            theSchema = sFact.newSchema(bindingsFileXSDSource);
            Validator validator = theSchema.newValidator();

            StreamSource ss = new StreamSource(src);
            validator.validate(ss);
        } catch (Exception e) {
            e.printStackTrace();
            if (e.getMessage() == null) {
                result = "An unknown exception occurred.";
            }
            result = e.getMessage();
        }
        assertTrue("Schema validation failed unxepectedly: " + result, result == null);
    }

    /**
     * Compare two schemas for equality.
     *
     */
    public static void compareSchemas(File testSchema, File controlSchema) {
        if (testSchema == null || controlSchema == null) {
            fail("Can't compare null schema file.");
        }
        try {
            DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
            builderFactory.setIgnoringElementContentWhitespace(true);
            builderFactory.setNamespaceAware(true);
            DocumentBuilder parser = builderFactory.newDocumentBuilder();
            InputStream stream = new FileInputStream(testSchema);
            Document control = parser.parse(stream);
            stream = new FileInputStream(controlSchema);
            Document test = parser.parse(stream);
            JAXBXMLComparer xmlComparer = new JAXBXMLComparer();
            if (!xmlComparer.isSchemaEqual(control, test)) {
                fail("The test schema did not match the control schema.\nEXPECTED:\n" + getFileAsText(controlSchema) + "\nACTUAL:\n" + getFileAsText(testSchema));
            }
        } catch (Exception x) {
            fail("An error occurred during schema comparison: " + x.getMessage());
        }
    }

    /**
     * Compare two schemas for equality.
     *
     */
    public static void compareSchemas(String testSchema, File controlSchema) {
        if (testSchema == null || controlSchema == null) {
            fail("Can't compare null schema file.");
        }
        try {
            DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
            builderFactory.setIgnoringElementContentWhitespace(true);
            builderFactory.setNamespaceAware(true);
            DocumentBuilder parser = builderFactory.newDocumentBuilder();
            InputStream stream = new FileInputStream(controlSchema);

            Document control = parser.parse(stream);

            InputSource source = new InputSource(new StringReader(testSchema));
            Document test = parser.parse(source);

            JAXBXMLComparer xmlComparer = new JAXBXMLComparer();
            if (!xmlComparer.isSchemaEqual(control, test)) {
                fail("The test schema did not match the control schema.\nEXPECTED:\n" + getFileAsText(controlSchema) + "\nACTUAL:\n" + testSchema);
            }
        } catch (Exception x) {
            fail("An error occurred during schema comparison: " + x.getMessage());
        }
    }

    public static boolean compareDocuments(Document ctrlDoc, Document testDoc) {
        return new XMLComparer().isNodeEqual(ctrlDoc, testDoc);
    }

    public static String getFileAsText(String filename) {
        return getFileAsText(new File(filename));
    }

    public static String getFileAsText(File file) {
        StringBuilder text = new StringBuilder();
        String NL = System.getProperty("line.separator");
        Scanner scanner = null;
        try {
            scanner = new Scanner(new FileInputStream(file));
            while (scanner.hasNextLine()) {
                text.append(scanner.nextLine() + NL);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            scanner.close();
        }
        return text.toString();
    }

    /**
     * SchemaOutputResolver for writing out the generated schema.  Returns a StreamResult
     * wrapping a File.
     *
     */
    public static class MySchemaOutputResolver extends SchemaOutputResolver {
        // keep a list of processed schemas for the validation phase of the test(s)
        public Map<String, File> schemaFiles;

        public MySchemaOutputResolver() {
            schemaFiles = new HashMap<String, File>();
        }

        @Override
        public Result createOutput(String namespaceURI, String suggestedFileName) throws IOException {
            //return new StreamResult(System.out);
            if (namespaceURI == null) {
                namespaceURI = EMPTY_NAMESPACE;
            }

            File schemaFile = new File(tmpdir, suggestedFileName);
            schemaFiles.put(namespaceURI, schemaFile);
            return new StreamResult(schemaFile);
        }
    }

    /**
     * SchemaOutputResolver for writing out the generated schema.  Returns a StreamResult
     * wrapping a StringWriter.
     *
     */
    public static class MyStreamSchemaOutputResolver extends SchemaOutputResolver {
        // keep a list of processed schemas for the validation phase of the test(s)
        public Map<String, Writer> schemaFiles;

        public MyStreamSchemaOutputResolver() {
            schemaFiles = new HashMap<String, Writer>();
        }

        @Override
        public Result createOutput(String namespaceURI, String suggestedFileName) throws IOException {
            //return new StreamResult(System.out);
            if (namespaceURI == null) {
                namespaceURI = EMPTY_NAMESPACE;
            }

            StringWriter sw = new StringWriter();
            schemaFiles.put(namespaceURI, sw);
            Result res = new StreamResult(sw);
            res.setSystemId(suggestedFileName);
            return res;
        }
    }

    /**
     * Convenience method that returns a newly created XMLContext based on an array of classes.
     *
     */
    protected XMLContext createXmlContext(Class<?>[] classes) {
        try {
            ClassLoader classLoader = new JaxbClassLoader(Thread.currentThread().getContextClassLoader());
            Generator generator = new Generator(new JavaModelInputImpl(classes, new JavaModelImpl(classLoader)));
            CoreProject proj = generator.generateProject();
            ConversionManager manager = new ConversionManager();
            manager.setLoader(classLoader);
            for (Iterator<ClassDescriptor> descriptorIt = proj.getOrderedDescriptors().iterator(); descriptorIt.hasNext(); ) {
                ClassDescriptor descriptor = descriptorIt.next();
                if (descriptor.getJavaClass() == null) {
                    descriptor.setJavaClass(manager.convertClassNameToClass(descriptor.getJavaClassName()));
                }
            }
            return new XMLContext((Project)proj, classLoader);
        } catch (Exception e) {
            e.printStackTrace();
            fail("XmlContext creation failed");
        }
        return null;
    }

    protected Document getControlDocument(String xmlResource) throws Exception {
        InputStream inputStream = ClassLoader.getSystemResourceAsStream(xmlResource);
        Document document = parser.parse(inputStream);
        OXTestCase.removeEmptyTextNodes(document);
        return document;
    }

    /**
     * Convenience method for creating a JAXBContext.  The XML document is
     * validated against the metadata schema.
     *
     */
    public JAXBContext createContext(Class<?>[] classes, String contextPath, String metadataFile) throws JAXBException {
        // validate instance document against the metadata schema
        InputStream iStream = loader.getResourceAsStream(metadataFile);
        InputStream iStreamCopy = loader.getResourceAsStream(metadataFile);
        if (iStream == null) {
            fail("Couldn't load metadata file [" + metadataFile + "]");
        }
        HashMap<String, Source> metadataSourceMap = new HashMap<>();
        metadataSourceMap.put(contextPath, new StreamSource(iStream));
        Map<String, Object> properties = new HashMap<>();
        properties.put(JAXBContextProperties.OXM_METADATA_SOURCE, metadataSourceMap);
        MySchemaOutputResolver outputResolver = new MySchemaOutputResolver();

        validateBindingsFileAgainstSchema(iStreamCopy);

        // create the JAXBContext
        jaxbContext = (JAXBContext) JAXBContextFactory.createContext(classes, properties, loader);
        return jaxbContext;
    }
}
