/*
 * JBoss, Home of Professional Open Source
 * Copyright 2015, Red Hat, Inc., and individual contributors as indicated
 * by the @authors tag.
 *
 * Licensed 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.jboss.as.subsystem.test.xml;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.jboss.logging.Logger;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSResourceResolver;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Local entity resolver to handle standard J2EE DTDs and Schemas as well as JBoss
 * specific DTDs.
 * <p>
 * Function boolean isEntityResolved() is here to avoid validation errors in
 * descriptors that do not have a DOCTYPE declaration.
 *
 * @author Scott.Stark@jboss.org
 * @author Thomas.Diesler@jboss.org
 * @author Dimitris.Andreadis@jboss.org
 * @version $Revision$
 */
@SuppressWarnings("unchecked")
public class JBossEntityResolver implements EntityResolver, LSResourceResolver {
    private static final Logger log = Logger.getLogger(JBossEntityResolver.class);

    /**
     * A class wide Map<String, String> of publicId/systemId to dtd/xsd file
     */
    private static Map entities = new ConcurrentHashMap();
    /**
     * A class flag indicating whether an attempt to resolve a systemID as a
     * non-file URL should produce a warning rather than a trace level log msg.
     */
    private static boolean warnOnNonFileURLs;

    /**
     * A local entities map that overrides the class level entities
     */
    private Map localEntities;

    private ThreadLocal<Boolean> entityResolved = new ThreadLocal<Boolean>();

    static {
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                warnOnNonFileURLs = new Boolean(System.getProperty("org.jboss.resolver.warning", "false")).booleanValue();
                return null;
            }
        });

        // xml
        registerEntity("-//W3C//DTD/XMLSCHEMA 200102//EN", "XMLSchema.dtd");
        registerEntity("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
        registerEntity("datatypes", "datatypes.dtd"); // This dtd doesn't have a publicId - see XMLSchema.dtd
        registerEntity("http://www.w3.org/XML/1998/namespace", "xml.xsd");
        registerEntity("http://www.w3.org/2001/xml.xsd", "xml.xsd");
        registerEntity("http://www.w3.org/2005/05/xmlmime", "xml-media-types.xsd");

        // Java EE common
        registerEntity("http://java.sun.com/xml/ns/j2ee/j2ee_1_4.xsd", "j2ee_1_4.xsd");
        registerEntity("http://java.sun.com/xml/ns/javaee/javaee_5.xsd", "javaee_5.xsd");
        // JBoss common
        registerEntity("http://www.jboss.org/j2ee/schema/jboss-common_5_1.xsd", "jboss-common_5_1.xsd");

        // Java EE WS
        registerEntity("http://schemas.xmlsoap.org/soap/encoding/", "soap-encoding_1_1.xsd");
        registerEntity("http://www.ibm.com/webservices/xsd/j2ee_web_services_client_1_1.xsd", "j2ee_web_services_client_1_1.xsd");
        registerEntity("http://www.ibm.com/webservices/xsd/j2ee_web_services_1_1.xsd", "j2ee_web_services_1_1.xsd");
        registerEntity("http://www.ibm.com/webservices/xsd/j2ee_jaxrpc_mapping_1_1.xsd", "j2ee_jaxrpc_mapping_1_1.xsd");
        registerEntity("http://java.sun.com/xml/ns/javaee/javaee_web_services_client_1_2.xsd", "javaee_web_services_client_1_2.xsd");

        // ejb
        registerEntity("-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 1.1//EN", "ejb-jar_1_1.dtd");
        registerEntity("-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 2.0//EN", "ejb-jar_2_0.dtd");
        registerEntity("http://java.sun.com/xml/ns/j2ee/ejb-jar_2_1.xsd", "ejb-jar_2_1.xsd");
        // ejb3
        registerEntity("http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd", "ejb-jar_3_0.xsd");
        // jboss ejb2
        registerEntity("-//JBoss//DTD JBOSS//EN", "jboss.dtd");
        registerEntity("-//JBoss//DTD JBOSS 2.4//EN", "jboss_2_4.dtd");
        registerEntity("-//JBoss//DTD JBOSS 3.0//EN", "jboss_3_0.dtd");
        registerEntity("-//JBoss//DTD JBOSS 3.2//EN", "jboss_3_2.dtd");
        registerEntity("-//JBoss//DTD JBOSS 4.0//EN", "jboss_4_0.dtd");
        registerEntity("-//JBoss//DTD JBOSS 4.2//EN", "jboss_4_2.dtd");
        registerEntity("-//JBoss//DTD JBOSS 5.0//EN", "jboss_5_0.dtd");
        registerEntity("-//JBoss//DTD JBOSS 5.1.EAP//EN", "jboss_5_1_eap.dtd");
        registerEntity("-//JBoss//DTD JBOSS 6.0//EN", "jboss_6_0.dtd");
        registerEntity("-//JBoss//DTD JBOSSCMP-JDBC 3.0//EN", "jbosscmp-jdbc_3_0.dtd");
        registerEntity("-//JBoss//DTD JBOSSCMP-JDBC 3.2//EN", "jbosscmp-jdbc_3_2.dtd");
        registerEntity("-//JBoss//DTD JBOSSCMP-JDBC 4.0//EN", "jbosscmp-jdbc_4_0.dtd");
        registerEntity("-//JBoss//DTD JBOSSCMP-JDBC 4.2//EN", "jbosscmp-jdbc_4_2.dtd");
        // jboss ejb3
        registerEntity("http://www.jboss.org/j2ee/schema/jboss_5_0.xsd", "jboss_5_0.xsd");
        registerEntity("http://www.jboss.org/j2ee/schema/jboss_5_1.xsd", "jboss_5_1.xsd");

        // ear stuff
        registerEntity("-//Sun Microsystems, Inc.//DTD J2EE Application 1.2//EN", "application_1_2.dtd");
        registerEntity("-//Sun Microsystems, Inc.//DTD J2EE Application 1.3//EN", "application_1_3.dtd");
        registerEntity("-//Sun Microsystems, Inc.//DTD J2EE Application Client 1.3//EN", "application-client_1_3.dtd");
        registerEntity("http://java.sun.com/xml/ns/j2ee/application_1_4.xsd", "application_1_4.xsd");
        registerEntity("http://java.sun.com/xml/ns/javaee/application_5.xsd", "application_5.xsd");
        // jboss-app
        registerEntity("-//JBoss//DTD J2EE Application 1.3//EN", "jboss-app_3_0.dtd");
        registerEntity("-//JBoss//DTD J2EE Application 1.3V2//EN", "jboss-app_3_2.dtd");
        registerEntity("-//JBoss//DTD J2EE Application 1.4//EN", "jboss-app_4_0.dtd");
        registerEntity("-//JBoss//DTD J2EE Application 4.2//EN", "jboss-app_4_2.dtd");
        registerEntity("-//JBoss//DTD Java EE Application 5.0//EN", "jboss-app_5_0.dtd");

        // connector descriptors
        registerEntity("-//Sun Microsystems, Inc.//DTD Connector 1.0//EN", "connector_1_0.dtd");
        registerEntity("http://java.sun.com/xml/ns/j2ee/connector_1_5.xsd", "connector_1_5.xsd");
        registerEntity("http://java.sun.com/xml/ns/j2ee/connector_1_6.xsd", "connector_1_6.xsd");
        // jboss-ds
        registerEntity("-//JBoss//DTD JBOSS JCA Config 1.0//EN", "jboss-ds_1_0.dtd");
        registerEntity("-//JBoss//DTD JBOSS JCA Config 1.5//EN", "jboss-ds_1_5.dtd");
        registerEntity("http://www.jboss.org/j2ee/schema/jboss-ds_5_0.xsd", "jboss-ds_5_0.xsd");
        // jboss-ra
        registerEntity("http://www.jboss.org/j2ee/schema/jboss-ra_1_0.xsd", "jboss-ra_1_0.xsd");

        // war meta-data
        registerEntity("-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN", "web-app_2_2.dtd");
        registerEntity("-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN", "web-app_2_3.dtd");
        registerEntity("http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd", "web-app_2_4.xsd");
        registerEntity("http://java.sun.com/xml/ns/j2ee/web-app_2_5.xsd", "web-app_2_5.xsd");
        registerEntity("http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd", "web-app_3_0.xsd");
        // jboss-web
        registerEntity("-//JBoss//DTD Web Application 2.2//EN", "jboss-web.dtd");
        registerEntity("-//JBoss//DTD Web Application 2.3//EN", "jboss-web_3_0.dtd");
        registerEntity("-//JBoss//DTD Web Application 2.3V2//EN", "jboss-web_3_2.dtd");
        registerEntity("-//JBoss//DTD Web Application 2.4//EN", "jboss-web_4_0.dtd");
        registerEntity("-//JBoss//DTD Web Application 4.2//EN", "jboss-web_4_2.dtd");
        registerEntity("-//JBoss//DTD Web Application 5.0//EN", "jboss-web_5_0.dtd");
        registerEntity("http://www.jboss.org/j2ee/schema/jboss-web_5_1.xsd", "jboss-web_5_1.xsd");

        // application client
        registerEntity("http://java.sun.com/xml/ns/j2ee/application-client_1_4.xsd", "application-client_1_4.xsd");
        registerEntity("http://java.sun.com/xml/ns/javaee/application-client_5.xsd", "application-client_5.xsd");
        // jboss-client
        registerEntity("-//JBoss//DTD Application Client 3.2//EN", "jboss-client_3_2.dtd");
        registerEntity("-//JBoss//DTD Application Client 4.0//EN", "jboss-client_4_0.dtd");
        registerEntity("-//JBoss//DTD Application Client 4.2//EN", "jboss-client_4_2.dtd");
        registerEntity("-//JBoss//DTD Application Client 5.0//EN", "jboss-client_5_0.dtd");
        registerEntity("http://www.jboss.org/j2ee/schema/jboss-client_5_1.xsd", "jboss-client_5_1.xsd");

        // jboss-specific
        registerEntity("-//JBoss//DTD Web Service Reference 4.0//EN", "service-ref_4_0.dtd");
        registerEntity("-//JBoss//DTD Web Service Reference 4.2//EN", "service-ref_4_2.dtd");
        registerEntity("-//JBoss//DTD Web Service Reference 5.0//EN", "service-ref_5_0.dtd");
        registerEntity("-//JBoss//DTD MBean Service 3.2//EN", "jboss-service_3_2.dtd");
        registerEntity("-//JBoss//DTD MBean Service 4.0//EN", "jboss-service_4_0.dtd");
        registerEntity("-//JBoss//DTD MBean Service 4.2//EN", "jboss-service_4_2.dtd");
        registerEntity("-//JBoss//DTD MBean Service 5.0//EN", "jboss-service_5_0.dtd");
        registerEntity("-//JBoss//DTD JBOSS XMBEAN 1.0//EN", "jboss_xmbean_1_0.dtd");
        registerEntity("-//JBoss//DTD JBOSS XMBEAN 1.1//EN", "jboss_xmbean_1_1.dtd");
        registerEntity("-//JBoss//DTD JBOSS XMBEAN 1.2//EN", "jboss_xmbean_1_2.dtd");
        registerEntity("-//JBoss//DTD JBOSS Security Config 3.0//EN", "security_config.dtd");
        registerEntity("http://www.jboss.org/j2ee/schema/security-config_4_0.xsd", "security-config_4_0.xsd");
        registerEntity("urn:jboss:aop-deployer", "aop-deployer_1_1.xsd");
        registerEntity("urn:jboss:aop-beans:1.0", "aop-beans_1_0.xsd");
        registerEntity("urn:jboss:bean-deployer", "bean-deployer_1_0.xsd");
        registerEntity("urn:jboss:bean-deployer:2.0", "bean-deployer_2_0.xsd");
        registerEntity("urn:jboss:javabean:1.0", "javabean_1_0.xsd");
        registerEntity("urn:jboss:javabean:2.0", "javabean_2_0.xsd");
        registerEntity("urn:jboss:spring-beans:2.0", "mc-spring-beans_2_0.xsd");
        registerEntity("urn:jboss:policy:1.0", "policy_1_0.xsd");
        registerEntity("urn:jboss:seam-components:1.0", "seam-components_1_0.xsd");
        registerEntity("urn:jboss:security-config:4.1", "security-config_4_1.xsd");
        registerEntity("urn:jboss:security-config:5.0", "security-config_5_0.xsd");
        registerEntity("urn:jboss:jndi-binding-service:1.0", "jndi-binding-service_1_0.xsd");
        registerEntity("urn:jboss:user-roles:1.0", "user-roles_1_0.xsd");
    }

    /**
     * Obtain a read-only view of the current entity map.
     *
     * @return Map<String, String> of the publicID/systemID to dtd/schema file name
     */
    public static Map getEntityMap() {
        return Collections.unmodifiableMap(entities);
    }

    public static boolean isWarnOnNonFileURLs() {
        return warnOnNonFileURLs;
    }

    public static void setWarnOnNonFileURLs(boolean warnOnNonFileURLs) {
        JBossEntityResolver.warnOnNonFileURLs = warnOnNonFileURLs;
    }

    /**
     * Register the mapping from the public id/system id to the dtd/xsd file
     * name. This overwrites any existing mapping.
     *
     * @param id          the DOCTYPE public id or system id such as
     *                    "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 1.1//EN"
     * @param dtdFileName the simple dtd/xsd file name, "ejb-jar.dtd"
     */
    public static void registerEntity(String id, String dtdFileName) {
        entities.put(id, dtdFileName);
    }

    /**
     * Register the mapping from the public id/system id to the dtd/xsd file
     * name. This overwrites any existing mapping.
     *
     * @param id          the DOCTYPE public id or system id such as
     *                    "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 1.1//EN"
     * @param dtdOrSchema the simple dtd/xsd file name, "ejb-jar.dtd"
     */
    public synchronized void registerLocalEntity(String id, String dtdOrSchema) {
        if (localEntities == null) { localEntities = new ConcurrentHashMap(); }
        localEntities.put(id, dtdOrSchema);
    }

    /**
     * Returns DTD/Schema inputSource. The resolution logic is:
     * <p>
     * 1. Check the publicId against the current registered values in the class
     * mapping of entity name to dtd/schema file name. If found, the resulting
     * file name is passed to the loadClasspathResource to locate the file as a
     * classpath resource.
     * <p>
     * 2. Check the systemId against the current registered values in the class
     * mapping of entity name to dtd/schema file name. If found, the resulting
     * file name is passed to the loadClasspathResource to locate the file as a
     * classpath resource.
     * <p>
     * 3. Strip the systemId name down to the simple file name by removing an URL
     * style path elements (myschemas/x.dtd becomes x.dtd), and call
     * loadClasspathResource to locate the simple file name as a classpath resource.
     * <p>
     * 4. Attempt to resolve the systemId as a URL from which the schema can be
     * read. If the URL input stream can be opened this returned as the resolved
     * input.
     *
     * @param publicId - Public ID of DTD, or null if it is a schema
     * @param systemId - the system ID of DTD or Schema
     * @return InputSource of entity
     */
    public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
        entityResolved.set(Boolean.FALSE);

        // nothing to resolve
        if (publicId == null && systemId == null) { return null; }

        boolean trace = log.isTraceEnabled();

        boolean resolvePublicIdFirst = true;
        if (publicId != null && systemId != null) {
            String registeredSystemId = null;
            if (localEntities != null) { registeredSystemId = (String) localEntities.get(publicId); }
            if (registeredSystemId == null) { registeredSystemId = (String) entities.get(publicId); }

            if (registeredSystemId != null && !registeredSystemId.equals(systemId)) {
                resolvePublicIdFirst = false;
                if (trace) {
                    log.trace("systemId argument '" + systemId + "' for publicId '" +
                            publicId + "' is different from the registered systemId '" +
                            registeredSystemId + "', resolution will be based on the argument");
                }
            }
        }

        InputSource inputSource = null;

        if (resolvePublicIdFirst) {
            // Look for a registered publicID
            inputSource = resolvePublicID(publicId, trace);
        }

        if (inputSource == null) {
            // Try to resolve the systemID from the registry
            inputSource = resolveSystemID(systemId, trace);
        }

        if (inputSource == null) {
            // Try to resolve the systemID as a classpath reference under dtd or schema
            inputSource = resolveClasspathName(systemId, trace);
        }

        if (inputSource == null) {
            // Try to resolve the systemID as a absolute URL
            inputSource = resolveSystemIDasURL(systemId, trace);
        }

        entityResolved.set(new Boolean(inputSource != null));

        if (inputSource == null) { log.debug("Cannot resolve [publicID=" + publicId + ",systemID=" + systemId + "]"); }

        return inputSource;
    }

    /**
     * Returns the boolean value to inform id DTD was found in the XML file or not
     *
     * @return boolean - true if DTD was found in XML
     */
    public boolean isEntityResolved() {
        Boolean value = entityResolved.get();
        return value != null ? value.booleanValue() : false;
    }

    /**
     * Load the schema from the class entity to schema file mapping.
     *
     * @param publicId - the public entity name of the schema
     * @param trace    - trace level logging flag
     * @return the InputSource for the schema file found on the classpath, null
     * if the publicId is not registered or found.
     * @see #registerEntity(String, String)
     */
    protected InputSource resolvePublicID(String publicId, boolean trace) {
        if (publicId == null) { return null; }

        if (trace) { log.trace("resolvePublicID, publicId=" + publicId); }

        InputSource inputSource = null;

        String filename = null;
        if (localEntities != null) { filename = (String) localEntities.get(publicId); }
        if (filename == null) { filename = (String) entities.get(publicId); }

        if (filename != null) {
            if (trace) { log.trace("Found entity from publicId=" + publicId + " fileName=" + filename); }

            InputStream ins = loadClasspathResource(filename, trace);
            if (ins != null) {
                inputSource = new InputSource(ins);
                inputSource.setPublicId(publicId);
            } else {
                log.trace("Cannot load publicId from classpath resource: " + filename);

                // Try the file name as a URI
                inputSource = resolveSystemIDasURL(filename, trace);

                if (inputSource == null) { log.warn("Cannot load publicId from resource: " + filename); }
            }
        }

        return inputSource;
    }

    /**
     * Attempt to use the systemId as a URL from which the schema can be read. This
     * checks to see whether the systemId is a key to an entry in the class
     * entity map.
     *
     * @param systemId - the systemId
     * @param trace    - trace level logging flag
     * @return the URL InputSource if the URL input stream can be opened, null
     * if the systemId is not a URL or could not be opened.
     */
    protected InputSource resolveSystemID(String systemId, boolean trace) {
        if (systemId == null) { return null; }

        if (trace) { log.trace("resolveSystemID, systemId=" + systemId); }

        InputSource inputSource = null;

        // Try to resolve the systemId as an entity key
        String filename = null;
        if (localEntities != null) { filename = (String) localEntities.get(systemId); }
        if (filename == null) { filename = (String) entities.get(systemId); }

        if (filename != null) {
            if (trace) { log.trace("Found entity systemId=" + systemId + " fileName=" + filename); }

            InputStream ins = loadClasspathResource(filename, trace);
            if (ins != null) {
                inputSource = new InputSource(ins);
                inputSource.setSystemId(systemId);
            } else {
                log.warn("Cannot load systemId from resource: " + filename);
            }
        }

        return inputSource;
    }

    /**
     * Attempt to use the systemId as a URL from which the schema can be read. This
     * uses the systemID as a URL.
     *
     * @param systemId - the systemId
     * @param trace    - trace level logging flag
     * @return the URL InputSource if the URL input stream can be opened, null
     * if the systemId is not a URL or could not be opened.
     */
    protected InputSource resolveSystemIDasURL(String systemId, boolean trace) {
        if (systemId == null) { return null; }

        if (trace) { log.trace("resolveSystemIDasURL, systemId=" + systemId); }

        InputSource inputSource = null;

        // Try to use the systemId as a URL to the schema
        try {
            if (trace) { log.trace("Trying to resolve systemId as a URL"); }

            URL url = new URL(systemId);
            if (warnOnNonFileURLs
                    && url.getProtocol().equalsIgnoreCase("file") == false
                    && url.getProtocol().equalsIgnoreCase("vfszip") == false) {
                log.warn("Trying to resolve systemId as a non-file URL: " + systemId);
            }

            InputStream ins = url.openStream();
            if (ins != null) {
                inputSource = new InputSource(ins);
                inputSource.setSystemId(systemId);
            } else {
                log.warn("Cannot load systemId as URL: " + systemId);
            }

            if (trace) { log.trace("Resolved systemId as a URL"); }
        } catch (MalformedURLException ignored) {
            if (trace) { log.trace("SystemId is not a url: " + systemId, ignored); }
        } catch (IOException e) {
            if (trace) { log.trace("Failed to obtain URL.InputStream from systemId: " + systemId, e); }
        }
        return inputSource;
    }

    /**
     * Resolve the systemId as a classpath resource. If not found, the
     * systemId is simply used as a classpath resource name.
     *
     * @param systemId - the system ID of DTD or Schema
     * @param trace    - trace level logging flag
     * @return the InputSource for the schema file found on the classpath, null
     * if the systemId is not registered or found.
     */
    protected InputSource resolveClasspathName(String systemId, boolean trace) {
        if (systemId == null) { return null; }

        if (trace) { log.trace("resolveClasspathName, systemId=" + systemId); }
        String filename = systemId;
        // Parse the systemId as a uri to get the final path component
        try {
            URI url = new URI(systemId);
            String path = url.getPath();
            if (path == null) { path = url.getSchemeSpecificPart(); }
            int slash = path.lastIndexOf('/');
            if (slash >= 0) { filename = path.substring(slash + 1); } else { filename = path; }

            if (filename.length() == 0) { return null; }

            if (trace) { log.trace("Mapped systemId to filename: " + filename); }
        } catch (URISyntaxException e) {
            if (trace) { log.trace("systemId: is not a URI, using systemId as resource", e); }
        }

        // Resolve the filename as a classpath resource
        InputStream is = loadClasspathResource(filename, trace);
        InputSource inputSource = null;
        if (is != null) {
            inputSource = new InputSource(is);
            inputSource.setSystemId(systemId);
        }
        return inputSource;
    }

    /**
     * Look for the resource name on the thread context loader resource path. This
     * first simply tries the resource name as is, and if not found, the resource
     * is prepended with either "dtd/" or "schema/" depending on whether the
     * resource ends in ".dtd" or ".xsd".
     *
     * @param resource - the classpath resource name of the schema
     * @param trace    - trace level logging flag
     * @return the resource InputStream if found, null if not found.
     */
    protected InputStream loadClasspathResource(String resource, boolean trace) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        URL url = loader.getResource(resource);
        if (url == null) {
         /* Prefix the simple filename with the schema type patch as this is the
               naming convention for the jboss bundled schemas.
            */
            if (resource.endsWith(".dtd")) { resource = "dtd/" + resource; } else if (resource.endsWith(".xsd")) { resource = "schema/" + resource; }
            url = loader.getResource(resource);
        }

        InputStream inputStream = null;
        if (url != null) {
            if (trace) { log.trace(resource + " maps to URL: " + url); }
            try {
                inputStream = url.openStream();
            } catch (IOException e) {
                log.debug("Failed to open url stream", e);
            }
        }
        return inputStream;
    }

    public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId, String baseURI) {
        InputSource inputSource = null;
        try {
            inputSource = resolveEntity(publicId, systemId);
        } catch (Exception e) {
            log.debug("Failed to resolve resource", e);
        }
        LSInput result = null;
        if (inputSource != null) {
            result = new LSInputImpl(publicId, systemId, baseURI, inputSource);
        }
        return result;
    }

    private static class LSInputImpl implements LSInput {

        private final String systemID;
        private final String publicID;
        private final String baseURI;
        private final InputSource inputSource;

        public LSInputImpl(String publicID, String systemID, String baseURI, InputSource inputSource) {
            this.inputSource = inputSource;
            this.systemID = systemID;
            this.publicID = publicID;
            this.baseURI = baseURI;
        }

        public Reader getCharacterStream() {
            return inputSource.getCharacterStream();
        }

        public void setCharacterStream(Reader characterStream) {
            throw new UnsupportedOperationException("not implemented");
        }

        public InputStream getByteStream() {
            return inputSource.getByteStream();
        }

        public void setByteStream(InputStream byteStream) {
            throw new UnsupportedOperationException("not implemented");
        }

        public String getStringData() {
            return null;
        }

        public void setStringData(String stringData) {
            throw new UnsupportedOperationException("not implemented");
        }

        public String getSystemId() {
            return systemID;
        }

        public void setSystemId(String systemId) {
            throw new UnsupportedOperationException("not implemented");
        }

        public String getPublicId() {
            return publicID;
        }

        public void setPublicId(String publicId) {
            throw new UnsupportedOperationException("not implemented");
        }

        public String getBaseURI() {
            return baseURI;
        }

        public void setBaseURI(String baseURI) {
            throw new UnsupportedOperationException("not implemented");
        }

        public String getEncoding() {
            return null;
        }

        public void setEncoding(String encoding) {
            throw new UnsupportedOperationException("not implemented");
        }

        public boolean getCertifiedText() {
            return false;
        }

        public void setCertifiedText(boolean certifiedText) {
            throw new UnsupportedOperationException("not implemented");
        }
    }
}
