/*
 *
 * $Revision$ $Date$
 *
 * This file is part of *** M y C o R e *** See http://www.mycore.de/ for
 * details.
 *
 * This program is free software; you can use it, redistribute it and / or
 * modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program, in a file called gpl.txt or license.txt. If not, write to the
 * Free Software Foundation Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307 USA
 */

package org.mycore.common.xml;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.servlet.ServletContext;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.URIResolver;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamSource;

import org.apache.http.Header;
import org.apache.http.client.cache.HttpCacheContext;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.cache.CacheConfig;
import org.apache.http.impl.client.cache.CachingHttpClients;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.Namespace;
import org.jdom2.input.SAXBuilder;
import org.jdom2.input.sax.XMLReaders;
import org.jdom2.transform.JDOMSource;
import org.mycore.access.MCRAccessManager;
import org.mycore.common.MCRCache;
import org.mycore.common.MCRClassTools;
import org.mycore.common.MCRConstants;
import org.mycore.common.MCRDeveloperTools;
import org.mycore.common.MCRException;
import org.mycore.common.MCRSessionMgr;
import org.mycore.common.MCRUsageException;
import org.mycore.common.config.MCRConfiguration2;
import org.mycore.common.config.MCRConfigurationDir;
import org.mycore.common.content.MCRByteContent;
import org.mycore.common.content.MCRContent;
import org.mycore.common.content.MCRPathContent;
import org.mycore.common.content.MCRSourceContent;
import org.mycore.common.content.MCRStreamContent;
import org.mycore.common.content.transformer.MCRContentTransformer;
import org.mycore.common.content.transformer.MCRParameterizedTransformer;
import org.mycore.common.content.transformer.MCRXSLTransformer;
import org.mycore.common.events.MCRShutdownHandler;
import org.mycore.common.xsl.MCRLazyStreamSource;
import org.mycore.common.xsl.MCRParameterCollector;
import org.mycore.datamodel.classifications2.MCRCategory;
import org.mycore.datamodel.classifications2.MCRCategoryDAO;
import org.mycore.datamodel.classifications2.MCRCategoryDAOFactory;
import org.mycore.datamodel.classifications2.MCRCategoryID;
import org.mycore.datamodel.classifications2.utils.MCRCategoryTransformer;
import org.mycore.datamodel.common.MCRDataURL;
import org.mycore.datamodel.common.MCRXMLMetadataManager;
import org.mycore.datamodel.ifs2.MCRMetadataStore;
import org.mycore.datamodel.ifs2.MCRMetadataVersion;
import org.mycore.datamodel.ifs2.MCRStoredMetadata;
import org.mycore.datamodel.metadata.MCRDerivate;
import org.mycore.datamodel.metadata.MCRFileMetadata;
import org.mycore.datamodel.metadata.MCRMetadataManager;
import org.mycore.datamodel.metadata.MCRObjectDerivate;
import org.mycore.datamodel.metadata.MCRObjectID;
import org.mycore.datamodel.niofs.MCRPath;
import org.mycore.datamodel.niofs.MCRPathXML;
import org.mycore.services.http.MCRHttpUtils;
import org.mycore.services.i18n.MCRTranslation;
import org.mycore.tools.MCRObjectFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

/**
 * Reads XML documents from various URI types. This resolver is used to read DTDs, XML Schema files, XSL document()
 * usages, xsl:include usages and MyCoRe Editor include declarations. DTDs and Schema files are read from the CLASSPATH
 * of the application when XML is parsed. XML document() calls and xsl:include calls within XSL stylesheets can be read
 * from URIs of type resource, webapp, file, session, query or mcrobject. MyCoRe editor include declarations can read
 * XML files from resource, webapp, file, session, http or https, query, or mcrobject URIs.
 *
 * @author Frank L\u00FCtzenkirchen
 * @author Thomas Scheffler (yagee)
 */
public final class MCRURIResolver implements URIResolver {
    static final Logger LOGGER = LogManager.getLogger(MCRURIResolver.class);

    static final String SESSION_OBJECT_NAME = "URI_RESOLVER_DEBUG";

    private static final String CONFIG_PREFIX = "MCR.URIResolver.";

    private static Map<String, URIResolver> SUPPORTED_SCHEMES;

    private static MCRResolverProvider EXT_RESOLVER;

    private static MCRURIResolver singleton;

    private static ServletContext context;

    static {
        try {
            EXT_RESOLVER = getExternalResolverProvider();
            singleton = new MCRURIResolver();
        } catch (Exception exc) {
            LOGGER.error("Unable to initialize MCRURIResolver", exc);
        }
    }

    /**
     * Creates a new MCRURIResolver
     */
    private MCRURIResolver() {
        SUPPORTED_SCHEMES = Collections.unmodifiableMap(getResolverMapping());
    }

    private static MCRResolverProvider getExternalResolverProvider() {
        return MCRConfiguration2.getClass(CONFIG_PREFIX + "ExternalResolver.Class")
            .map(c -> {
                try {
                    return (MCRResolverProvider) c.getDeclaredConstructor().newInstance();
                } catch (ReflectiveOperationException e) {
                    LOGGER.warn("Could not instantiate external Resolver class", e);
                    return null;
                }
            }).orElse(HashMap::new);
    }

    /**
     * Returns the MCRURIResolver singleton
     */
    public static MCRURIResolver instance() {
        return singleton;
    }

    /**
     * Initializes the MCRURIResolver for servlet applications.
     *
     * @param ctx
     *            the servlet context of this web application
     */
    public static synchronized void init(ServletContext ctx) {
        context = ctx;
    }

    public static Hashtable<String, String> getParameterMap(String key) {
        String[] param;
        StringTokenizer tok = new StringTokenizer(key, "&");
        Hashtable<String, String> params = new Hashtable<>();

        while (tok.hasMoreTokens()) {
            param = tok.nextToken().split("=");
            params.put(param[0], param.length >= 2 ? param[1] : "");
        }
        return params;
    }

    static URI resolveURI(String href, String base) {
        return Optional.ofNullable(base)
            .map(URI::create)
            .map(u -> u.resolve(href))
            .orElse(URI.create(href));
    }

    public static ServletContext getServletContext() {
        return context;
    }

    private HashMap<String, URIResolver> getResolverMapping() {
        final Map<String, URIResolver> extResolverMapping = EXT_RESOLVER.getURIResolverMapping();
        extResolverMapping.putAll(new MCRModuleResolverProvider().getURIResolverMapping());
        // set Map to final size with loadfactor: full
        HashMap<String, URIResolver> supportedSchemes = new HashMap<>(10 + extResolverMapping.size(), 1);
        // don't let interal mapping be overwritten
        supportedSchemes.putAll(extResolverMapping);
        supportedSchemes.put("webapp", new MCRWebAppResolver());
        supportedSchemes.put("ifs", new MCRIFSResolver());
        supportedSchemes.put("mcrfile", new MCRMCRFileResolver());
        supportedSchemes.put("mcrobject", new MCRObjectResolver());
        supportedSchemes.put("session", new MCRSessionResolver());
        supportedSchemes.put("access", new MCRACLResolver());
        supportedSchemes.put("resource", new MCRResourceResolver());
        supportedSchemes.put("localclass", new MCRLocalClassResolver());
        supportedSchemes.put("classification", new MCRClassificationResolver());
        supportedSchemes.put("buildxml", new MCRBuildXMLResolver());
        supportedSchemes.put("catchEx", new MCRExceptionAsXMLResolver());
        supportedSchemes.put("notnull", new MCRNotNullResolver());
        supportedSchemes.put("xslStyle", new MCRXslStyleResolver());
        supportedSchemes.put("xslTransform", new MCRLayoutTransformerResolver());
        supportedSchemes.put("xslInclude", new MCRXslIncludeResolver());
        supportedSchemes.put("xslImport", new MCRXslImportResolver());
        supportedSchemes.put("versioninfo", new MCRVersionInfoResolver());
        supportedSchemes.put("deletedMcrObject", new MCRDeletedObjectResolver());
        supportedSchemes.put("fileMeta", new MCRFileMetadataResolver());
        supportedSchemes.put("basket", new org.mycore.frontend.basket.MCRBasketResolver());
        supportedSchemes.put("language", new org.mycore.datamodel.language.MCRLanguageResolver());
        supportedSchemes.put("chooseTemplate", new MCRChooseTemplateResolver());
        supportedSchemes.put("redirect", new MCRRedirectResolver());
        supportedSchemes.put("data", new MCRDataURLResolver());
        supportedSchemes.put("i18n", new MCRI18NResolver());
        MCRRESTResolver restResolver = new MCRRESTResolver();
        supportedSchemes.put("http", restResolver);
        supportedSchemes.put("https", restResolver);
        supportedSchemes.put("file", new MCRFileResolver());
        return supportedSchemes;
    }

    /**
     * URI Resolver that resolves XSL document() or xsl:include calls.
     *
     * @see javax.xml.transform.URIResolver
     */
    public Source resolve(String href, String base) throws TransformerException {
        if (LOGGER.isDebugEnabled()) {
            if (base != null) {
                LOGGER.debug("Including {} from {}", href, base);
                addDebugInfo(href, base);
            } else {
                LOGGER.debug("Including {}", href);
                addDebugInfo(href, null);
            }
        }
        if (!href.contains(":")) {
            return tryResolveXSL(href, base);
        }

        String scheme = getScheme(href, base);

        URIResolver uriResolver = SUPPORTED_SCHEMES.get(scheme);
        if (uriResolver != null) {
            return uriResolver.resolve(href, base);
        } else { // try to handle as URL, use default resolver for file:// and
            try {
                InputSource entity = MCREntityResolver.instance().resolveEntity(null, href);
                if (entity != null) {
                    LOGGER.debug("Resolved via EntityResolver: {}", entity.getSystemId());
                    return new MCRLazyStreamSource(entity::getByteStream, entity.getSystemId());
                }
            } catch (IOException e) {
                LOGGER.debug("Error while resolving uri: {}", href);
            }
            // http://
            if (href.endsWith("/") && scheme.equals("file")) {
                //cannot stream directories
                return null;
            }
            StreamSource streamSource = new StreamSource();
            streamSource.setSystemId(href);
            return streamSource;
        }
    }

    private Source tryResolveXSL(String href, String base) throws TransformerException {
        if (href.endsWith(".xsl")) {
            final String uri = "resource:xsl/" + href;
            LOGGER.debug("Trying to resolve {} from uri {}", href, uri);
            return SUPPORTED_SCHEMES.get("resource").resolve(uri, base);
        }
        return null;
    }

    private void addDebugInfo(String href, String base) {
        MCRURIResolverFilter.uriList.get().add(href + " from " + base);
    }

    /**
     * Reads XML from URIs of various type.
     *
     * @param uri
     *            the URI where to read the XML from
     * @return the root element of the XML document
     */
    public Element resolve(String uri) {
        if (LOGGER.isDebugEnabled()) {
            addDebugInfo(uri, "JAVA method invocation");
        }
        MCRSourceContent content;
        try {
            content = MCRSourceContent.getInstance(uri);
            return content == null ? null : content.asXML().getRootElement().detach();
        } catch (Exception e) {
            throw new MCRException("Error while resolving " + uri, e);
        }
    }

    /**
     * Returns the protocol or scheme for the given URI.
     *
     * @param uri
     *            the URI to parse
     * @param base
     *            if uri is relative, resolve scheme from base parameter
     * @return the protocol/scheme part before the ":"
     */
    public String getScheme(String uri, String base) {
        StringTokenizer uriTokenizer = new StringTokenizer(uri, ":");
        if (uriTokenizer.hasMoreTokens()) {
            return uriTokenizer.nextToken();
        }
        if (base != null) {
            uriTokenizer = new StringTokenizer(base, ":");
            if (uriTokenizer.hasMoreTokens()) {
                return uriTokenizer.nextToken();
            }
        }
        return null;
    }

    URIResolver getResolver(String scheme) {
        if (SUPPORTED_SCHEMES.containsKey(scheme)) {
            return SUPPORTED_SCHEMES.get(scheme);
        }
        String msg = "Unsupported scheme type: " + scheme;
        throw new MCRUsageException(msg);
    }

    /**
     * Reads xml from an InputStream and returns the parsed root element.
     *
     * @param in
     *            the InputStream that contains the XML document
     * @return the root element of the parsed input stream
     */
    protected Element parseStream(InputStream in) throws JDOMException, IOException {
        SAXBuilder builder = new SAXBuilder(XMLReaders.NONVALIDATING);
        builder.setEntityResolver(MCREntityResolver.instance());

        return builder.build(in).getRootElement();
    }

    /**
     * provides a URI -- Resolver Mapping One can implement this interface to provide additional URI schemes this
     * MCRURIResolver should handle, too. To add your mapping you have to set the
     * <code>MCR.URIResolver.ExternalResolver.Class</code> property to the implementing class.
     *
     * @author Thomas Scheffler
     */
    public interface MCRResolverProvider {
        /**
         * provides a Map of URIResolver mappings. Key is the scheme, e.g. <code>http</code>, where value is an
         * implementation of {@link URIResolver}.
         *
         * @see URIResolver
         * @return a Map of URIResolver mappings
         */
        Map<String, URIResolver> getURIResolverMapping();
    }

    public interface MCRCacheableURIResolver extends URIResolver {
        MCRCacheableURIResponse getResponse(String href, String base) throws TransformerException;

        @Override
        default Source resolve(String href, String base) throws TransformerException {
            return getResponse(href, base).getSource();
        }

    }

    public interface MCRXslIncludeHrefs {
        List<String> getHrefs();
    }

    public static class MCRCacheableURIResponse {
        private Source source;

        private Supplier<Integer> hash;

        private boolean support;

        public MCRCacheableURIResponse(Source source, Supplier<Integer> hashSupplier,
            Supplier<Boolean> supportSupplier) {
            this.source = source;
            this.hash = hashSupplier;
            this.support = supportSupplier.get();
        }

        public Source getSource() {
            return source;
        }

        public int getHash() {
            return hash.get();
        }

        public boolean supportsHash() {
            return support;
        }
    }

    private static class MCRModuleResolverProvider implements MCRResolverProvider {
        private final Map<String, URIResolver> resolverMap = new HashMap<>();

        MCRModuleResolverProvider() {
            MCRConfiguration2.getSubPropertiesMap(CONFIG_PREFIX + "ModuleResolver.")
                .forEach(this::registerUriResolver);
        }

        public Map<String, URIResolver> getURIResolverMapping() {
            return resolverMap;
        }

        private void registerUriResolver(String scheme, String className) {
            try {
                resolverMap.put(scheme, MCRConfiguration2.instantiateClass(className));
            } catch (RuntimeException re) {
                throw new MCRException("Cannot instantiate " + className + " for URI scheme " + scheme, re);
            }
        }

    }

    private static class MCRFileResolver implements URIResolver {

        @Override
        public Source resolve(String href, String base) throws TransformerException {
            URI hrefURI = MCRURIResolver.resolveURI(href, base);
            if (!hrefURI.getScheme().equals("file")) {
                throw new TransformerException("Unsupport file uri scheme: " + hrefURI.getScheme());
            }
            Path path = Paths.get(hrefURI);
            StreamSource source;
            try {
                source = new StreamSource(Files.newInputStream(path), hrefURI.toASCIIString());
                return source;
            } catch (IOException e) {
                throw new TransformerException(e);
            }
        }
    }

    private static class MCRRESTResolver implements MCRCacheableURIResolver {

        private static final long MAX_OBJECT_SIZE = MCRConfiguration2.getLong(CONFIG_PREFIX + "REST.MaxObjectSize")
            .orElse(128 * 1024l);

        private static final int MAX_CACHE_ENTRIES = MCRConfiguration2.getInt(CONFIG_PREFIX + "REST.MaxCacheEntries")
            .orElse(1000);

        private static final int REQUEST_TIMEOUT = MCRConfiguration2.getInt(CONFIG_PREFIX + "REST.RequestTimeout")
            .orElse(30000);

        private CloseableHttpClient restClient;

        private org.apache.logging.log4j.Logger logger;

        MCRRESTResolver() {
            CacheConfig cacheConfig = CacheConfig.custom()
                .setMaxObjectSize(MAX_OBJECT_SIZE)
                .setMaxCacheEntries(MAX_CACHE_ENTRIES)
                .build();
            RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(REQUEST_TIMEOUT)
                .setSocketTimeout(REQUEST_TIMEOUT)
                .build();
            this.restClient = CachingHttpClients.custom()
                .setCacheConfig(cacheConfig)
                .setDefaultRequestConfig(requestConfig)
                .setUserAgent(MCRHttpUtils.getHttpUserAgent())
                .useSystemProperties()
                .build();
            MCRShutdownHandler.getInstance().addCloseable(this::close);
            this.logger = LogManager.getLogger();
        }

        private static int getHash(String lastModified, String eTag) {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((eTag == null) ? 0 : eTag.hashCode());
            result = prime * result + ((lastModified == null) ? 0 : lastModified.hashCode());
            return result;
        }

        public void close() {
            try {
                restClient.close();
            } catch (IOException e) {
                LogManager.getLogger().warn("Exception while closing http client.", e);
            }
        }

        @Override
        public MCRCacheableURIResponse getResponse(String href, String base) throws TransformerException {
            URI hrefURI = MCRURIResolver.resolveURI(href, base);
            InputStream responseStream;
            String eTag, lastModified;
            try {
                HttpCacheContext context = HttpCacheContext.create();
                HttpGet get = new HttpGet(hrefURI);
                CloseableHttpResponse response = restClient.execute(get, context);
                logger.debug(() -> {
                    String msg = hrefURI.toASCIIString() + ": ";
                    switch (context.getCacheResponseStatus()) {
                        case CACHE_HIT:
                            msg += "A response was generated from the cache with " +
                                "no requests sent upstream";
                            break;
                        case CACHE_MODULE_RESPONSE:
                            msg += "The response was generated directly by the " +
                                "caching module";
                            break;
                        case CACHE_MISS:
                            msg += "The response came from an upstream server";
                            break;
                        case VALIDATED:
                            msg += "The response was generated from the cache " +
                                "after validating the entry with the origin server";
                            break;
                    }
                    return msg;
                });
                eTag = Optional.ofNullable(response.getLastHeader("etag"))
                    .map(Header::getValue)
                    .orElse(null);
                lastModified = Optional.ofNullable(response.getLastHeader("last-modified"))
                    .map(Header::getValue)
                    .orElse(null);

                try (InputStream content = response.getEntity().getContent()) {
                    final Source source = new MCRStreamContent(content).getReusableCopy().getSource();
                    source.setSystemId(hrefURI.toASCIIString());
                    return new MCRCacheableURIResponse(source,
                        () -> getHash(lastModified, eTag), () -> lastModified != null || eTag != null);
                } catch (Exception e) {
                    throw new MCRException(e);
                } finally {
                    response.close();
                    get.reset();
                }
            } catch (IOException e) {
                throw new TransformerException(e);
            }
        }

    }

    private static class MCRObjectResolver implements URIResolver {

        /**
         * Reads local MCRObject with a given ID from the store.
         *
         * @param href
         *            for example, "mcrobject:DocPortal_document_07910401"
         * @returns XML representation from MCRXMLContainer
         */
        @Override
        public Source resolve(String href, String base) throws TransformerException {
            String id = href.substring(href.indexOf(":") + 1);
            LOGGER.debug("Reading MCRObject with ID {}", id);
            Map<String, String> params;
            StringTokenizer tok = new StringTokenizer(id, "?");
            id = tok.nextToken();

            if (tok.hasMoreTokens()) {
                params = getParameterMap(tok.nextToken());
            } else {
                params = Collections.emptyMap();
            }

            MCRObjectID mcrid = MCRObjectID.getInstance(id);
            try {
                MCRXMLMetadataManager xmlmm = MCRXMLMetadataManager.instance();
                MCRContent content = params.containsKey("r")
                    ? xmlmm.retrieveContent(mcrid, Long.valueOf(params.get("r")))
                    : xmlmm.retrieveContent(mcrid);
                if (content == null) {
                    return null;
                }
                LOGGER.debug("end resolving {}", href);
                return content.getSource();
            } catch (IOException e) {
                throw new TransformerException(e);
            }
        }

    }

    /**
     * Reads XML from a static file within the web application. the URI in the format webapp:path/to/servlet
     */
    private static class MCRWebAppResolver implements URIResolver {

        @Override
        public Source resolve(String href, String base) throws TransformerException {
            String path = href.substring(href.indexOf(":") + 1);
            if (path.charAt(0) != '/') {
                path = '/' + path;
            }

            if (MCRDeveloperTools.overrideActive()) {
                final Optional<Path> overriddenFilePath = MCRDeveloperTools.getOverriddenFilePath(path, true);
                if (overriddenFilePath.isPresent()) {
                    return new StreamSource(overriddenFilePath.get().toFile());
                }
            }

            LOGGER.debug("Reading xml from webapp {}", path);
            try {
                URL resource = context.getResource(path);
                if (resource != null) {
                    return new StreamSource(resource.toURI().toASCIIString());
                }
            } catch (Exception ex) {
                throw new TransformerException(ex);
            }
            LOGGER.error("File does not exist: {}", context.getRealPath(path));
            throw new TransformerException("Could not find web resource: " + path);
        }
    }

    private static class MCRChooseTemplateResolver implements URIResolver {

        private static Document getStylesheets(List<String> temps) {

            Element rootOut = new Element("stylesheet", MCRConstants.XSL_NAMESPACE).setAttribute("version", "1.0");
            Document jdom = new Document(rootOut);

            if (temps.isEmpty()) {
                return jdom;
            }

            for (String templateName : temps) {
                rootOut.addContent(
                    new Element("include", MCRConstants.XSL_NAMESPACE).setAttribute("href", templateName + ".xsl"));
            }

            // first template named "chooseTemplate" in chooseTemplate.xsl
            Element template = new Element("template", MCRConstants.XSL_NAMESPACE).setAttribute("name",
                "chooseTemplate");
            Element choose = new Element("choose", MCRConstants.XSL_NAMESPACE);
            // second template named "get.templates" in chooseTemplate.xsl
            Element template2 = new Element("template", MCRConstants.XSL_NAMESPACE).setAttribute("name",
                "get.templates");
            Element templates = new Element("templates");

            for (String templateName : temps) {
                // add elements in the first template
                Element when = new Element("when", MCRConstants.XSL_NAMESPACE).setAttribute("test",
                    "$template = '" + templateName + "'");
                when.addContent(
                    new Element("call-template", MCRConstants.XSL_NAMESPACE).setAttribute("name", templateName));
                choose.addContent(when);

                // add elements in the second template
                templates.addContent(new Element("template").setAttribute("category", "master").setText(templateName));
            }

            // first
            template.addContent(choose);
            rootOut.addContent(template);
            // second
            template2.addContent(templates);
            rootOut.addContent(template2);
            return jdom;
        }

        @Override
        public Source resolve(String href, String base) throws TransformerException {
            String type = href.substring(href.indexOf(":") + 1);
            String path = "/templates/" + type + "/";
            LOGGER.debug("Reading templates from {}", path);
            Set<String> resourcePaths = context.getResourcePaths(path);
            ArrayList<String> templates = new ArrayList<>();
            if (resourcePaths != null) {
                for (String resourcePath : resourcePaths) {
                    if (!resourcePath.endsWith("/")) {
                        //only handle directories
                        continue;
                    }
                    String templateName = resourcePath.substring(path.length(), resourcePath.length() - 1);
                    LOGGER.debug("Checking if template: {}", templateName);
                    if (templateName.contains("/")) {
                        continue;
                    }
                    templates.add(templateName);
                }
                Collections.sort(templates);
            }
            LOGGER.info("Found theses templates: {}", templates);
            return new JDOMSource(getStylesheets(templates));
        }

    }

    /**
     * Reads XML from the CLASSPATH of the application. the location of the file in the format resource:path/to/file
     */
    private static class MCRResourceResolver implements URIResolver {

        @Override
        public Source resolve(String href, String base) throws TransformerException {
            String path = href.substring(href.indexOf(":") + 1);
            URL resource = MCRConfigurationDir.getConfigResource(path);
            if (resource != null) {
                //have to use SAX here to resolve entities
                if (path.endsWith(".xsl")) {
                    XMLReader reader;
                    try {
                        reader = MCRXMLParserFactory.getNonValidatingParser().getXMLReader();
                    } catch (SAXException | ParserConfigurationException e) {
                        throw new TransformerException(e);
                    }
                    reader.setEntityResolver(MCREntityResolver.instance());
                    InputSource input = new InputSource(resource.toString());
                    SAXSource saxSource = new SAXSource(reader, input);
                    LOGGER.debug("include stylesheet: {}", saxSource.getSystemId());
                    return saxSource;
                }
                return MCRURIResolver.instance().resolve(resource.toString(), base);
            }
            return null;
        }
    }

    /**
     * Delivers a jdom Element created by any local class that implements URIResolver
     * interface. the class name of the file in the format localclass:org.mycore.ClassName?mode=getAll
     */
    private static class MCRLocalClassResolver implements URIResolver {

        @Override
        public Source resolve(String href, String base) throws TransformerException {
            String classname = href.substring(href.indexOf(":") + 1, href.indexOf("?"));
            Class<? extends URIResolver> cl = null;
            LogManager.getLogger(this.getClass()).debug("Loading Class: {}", classname);
            URIResolver resolver;
            try {
                cl = MCRClassTools.forName(classname);
                resolver = cl.getDeclaredConstructor().newInstance();
            } catch (Exception e) {
                throw new TransformerException(e);
            }
            return resolver.resolve(href, base);
        }

    }

    private static class MCRSessionResolver implements URIResolver {

        /**
         * Reads XML from URIs of type session:key. The method MCRSession.get( key ) is called and must return a JDOM
         * element.
         *
         * @see org.mycore.common.MCRSession#get(Object)
         * @param href
         *            the URI in the format session:key
         * @return the root element of the xml document
         */
        @Override
        public Source resolve(String href, String base) throws TransformerException {
            String key = href.substring(href.indexOf(":") + 1);
            LOGGER.debug("Reading xml from session using key {}", key);
            Element value = (Element) MCRSessionMgr.getCurrentSession().get(key);
            return new JDOMSource(value.clone());
        }

    }

    private static class MCRIFSResolver implements URIResolver {

        /**
         * Reads XML from a http or https URL.
         *
         * @param href
         *            the URL of the xml document
         * @return the root element of the xml document
         */
        @Override
        public Source resolve(String href, String base) throws TransformerException {
            LOGGER.debug("Reading xml from url {}", href);

            String path = href.substring(href.indexOf(":") + 1);

            int i = path.indexOf("?host");
            if (i > 0) {
                path = path.substring(0, i);
            }
            StringTokenizer st = new StringTokenizer(path, "/");

            String ownerID = st.nextToken();
            try {
                String aPath = MCRXMLFunctions.decodeURIPath(path.substring(ownerID.length() + 1));
                // TODO: make this more pretty
                if (ownerID.endsWith(":")) {
                    ownerID = ownerID.substring(0, ownerID.length() - 1);
                }
                LOGGER.debug("Get {} path: {}", ownerID, aPath);
                return new JDOMSource(MCRPathXML.getDirectoryXML(MCRPath.getPath(ownerID, aPath)));
            } catch (IOException | URISyntaxException e) {
                throw new TransformerException(e);
            }
        }
    }

    private static class MCRMCRFileResolver implements URIResolver {
        @Override
        public Source resolve(String href, String base) throws TransformerException {
            LOGGER.debug("Reading xml from MCRFile {}", href);
            MCRPath file = null;
            String id = href.substring(href.indexOf(":") + 1);
            if (id.contains("/")) {
                // assume thats a derivate with path
                try {
                    MCRObjectID derivateID = MCRObjectID.getInstance(id.substring(0, id.indexOf("/")));
                    String path = id.substring(id.indexOf("/"));
                    file = MCRPath.getPath(derivateID.toString(), path);
                } catch (MCRException exc) {
                    // just check if the id is valid, don't care about the exception
                }
            }
            if (file == null) {
                throw new TransformerException("mcrfile: Resolver needs a path: " + href);
            }
            try {
                return new MCRPathContent(file).getSource();
            } catch (Exception e) {
                throw new TransformerException(e);
            }
        }
    }

    private static class MCRACLResolver implements URIResolver {

        private static final String ACTION_PARAM = "action";

        private static final String OBJECT_ID_PARAM = "object";

        /**
         * Returns access controll rules as XML
         */
        public Source resolve(String href, String base) throws TransformerException {
            String key = href.substring(href.indexOf(":") + 1);
            LOGGER.debug("Reading xml from query result using key :{}", key);

            String[] param;
            StringTokenizer tok = new StringTokenizer(key, "&");
            Hashtable<String, String> params = new Hashtable<>();

            while (tok.hasMoreTokens()) {
                param = tok.nextToken().split("=");
                params.put(param[0], param[1]);
            }

            String action = params.get(ACTION_PARAM);
            String objId = params.get(OBJECT_ID_PARAM);

            if (action == null || objId == null) {
                return null;
            }

            Element container = new Element("servacls").setAttribute("class", "MCRMetaAccessRule");

            if (action.equals("all")) {
                for (String permission : MCRAccessManager.getPermissionsForID(objId)) {
                    // one pool Element under access per defined AccessRule in
                    // Pool
                    // for (Object-)ID
                    addRule(container, permission, MCRAccessManager.getAccessImpl().getRule(objId, permission));
                }
            } else {
                addRule(container, action, MCRAccessManager.getAccessImpl().getRule(objId, action));
            }

            return new JDOMSource(container);
        }

        private void addRule(Element root, String pool, Element rule) {
            if (rule != null && pool != null) {
                Element poolElement = new Element("servacl").setAttribute("permission", pool);
                poolElement.addContent(rule);
                root.addContent(poolElement);
            }
        }

    }

    private static class MCRClassificationResolver implements URIResolver {

        private static final Pattern EDITORFORMAT_PATTERN = Pattern.compile("(\\[)([^\\]]*)(\\])");

        private static final String FORMAT_CONFIG_PREFIX = CONFIG_PREFIX + "Classification.Format.";

        private static final String SORT_CONFIG_PREFIX = CONFIG_PREFIX + "Classification.Sort.";

        private static MCRCache<String, Element> categoryCache;

        private static MCRCategoryDAO DAO;

        static {
            try {
                DAO = MCRCategoryDAOFactory.getInstance();
                categoryCache = new MCRCache<>(
                    MCRConfiguration2.getInt(CONFIG_PREFIX + "Classification.CacheSize").orElse(1000),
                    "URIResolver categories");
            } catch (Exception exc) {
                LOGGER.error("Unable to initialize classification resolver", exc);
            }
        }

        MCRClassificationResolver() {
        }

        private static String getLabelFormat(String editorString) {
            Matcher m = EDITORFORMAT_PATTERN.matcher(editorString);
            if (m.find() && m.groupCount() == 3) {
                String formatDef = m.group(2);
                return MCRConfiguration2.getStringOrThrow(FORMAT_CONFIG_PREFIX + formatDef);
            }
            return null;
        }

        private static boolean shouldSortCategories(String classId) {
            return MCRConfiguration2.getBoolean(SORT_CONFIG_PREFIX + classId).orElse(true);
        }

        private static long getSystemLastModified() {
            long xmlLastModified = MCRXMLMetadataManager.instance().getLastModified();
            long classLastModified = DAO.getLastModified();
            return Math.max(xmlLastModified, classLastModified);
        }

        /**
         * returns a classification in a specific format. Syntax:
         * <code>classification:{editor[Complete]['['formatAlias']']|metadata}:{Levels}[:noEmptyLeaves]:{parents|
         * children}:{ClassID}[:CategID] formatAlias: MCRConfiguration property
         * MCR.UURResolver.Classification.Format.FormatAlias
         *
         * @param href
         *            URI in the syntax above
         * @return the root element of the XML document
         * @see MCRCategoryTransformer
         */
        public Source resolve(String href, String base) throws TransformerException {
            LOGGER.debug("start resolving {}", href);
            String cacheKey = getCacheKey(href);
            Element returns