View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.common.xml;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.PrintWriter;
24  import java.io.StringWriter;
25  import java.net.URI;
26  import java.net.URISyntaxException;
27  import java.net.URL;
28  import java.net.URLDecoder;
29  import java.nio.charset.StandardCharsets;
30  import java.nio.file.Files;
31  import java.nio.file.Path;
32  import java.nio.file.Paths;
33  import java.time.Instant;
34  import java.time.temporal.ChronoUnit;
35  import java.util.ArrayList;
36  import java.util.Collections;
37  import java.util.HashMap;
38  import java.util.Hashtable;
39  import java.util.List;
40  import java.util.Map;
41  import java.util.Optional;
42  import java.util.Set;
43  import java.util.StringTokenizer;
44  import java.util.regex.Matcher;
45  import java.util.regex.Pattern;
46  import java.util.stream.Collectors;
47  
48  import javax.xml.parsers.ParserConfigurationException;
49  import javax.xml.transform.Source;
50  import javax.xml.transform.TransformerException;
51  import javax.xml.transform.URIResolver;
52  import javax.xml.transform.sax.SAXSource;
53  import javax.xml.transform.stream.StreamSource;
54  
55  import org.apache.http.client.cache.HttpCacheContext;
56  import org.apache.http.client.config.RequestConfig;
57  import org.apache.http.client.methods.CloseableHttpResponse;
58  import org.apache.http.client.methods.HttpGet;
59  import org.apache.http.impl.client.CloseableHttpClient;
60  import org.apache.http.impl.client.cache.CacheConfig;
61  import org.apache.http.impl.client.cache.CachingHttpClients;
62  import org.apache.logging.log4j.LogManager;
63  import org.apache.logging.log4j.Logger;
64  import org.jdom2.Document;
65  import org.jdom2.Element;
66  import org.jdom2.JDOMException;
67  import org.jdom2.Namespace;
68  import org.jdom2.input.SAXBuilder;
69  import org.jdom2.input.sax.XMLReaders;
70  import org.jdom2.transform.JDOMSource;
71  import org.mycore.access.MCRAccessManager;
72  import org.mycore.common.MCRCache;
73  import org.mycore.common.MCRClassTools;
74  import org.mycore.common.MCRConstants;
75  import org.mycore.common.MCRDeveloperTools;
76  import org.mycore.common.MCRException;
77  import org.mycore.common.MCRSessionMgr;
78  import org.mycore.common.MCRUsageException;
79  import org.mycore.common.config.MCRConfiguration2;
80  import org.mycore.common.config.MCRConfigurationDir;
81  import org.mycore.common.content.MCRByteContent;
82  import org.mycore.common.content.MCRContent;
83  import org.mycore.common.content.MCRPathContent;
84  import org.mycore.common.content.MCRSourceContent;
85  import org.mycore.common.content.MCRStreamContent;
86  import org.mycore.common.content.transformer.MCRContentTransformer;
87  import org.mycore.common.content.transformer.MCRParameterizedTransformer;
88  import org.mycore.common.content.transformer.MCRXSLTransformer;
89  import org.mycore.common.events.MCRShutdownHandler;
90  import org.mycore.common.xsl.MCRLazyStreamSource;
91  import org.mycore.common.xsl.MCRParameterCollector;
92  import org.mycore.datamodel.classifications2.MCRCategory;
93  import org.mycore.datamodel.classifications2.MCRCategoryDAO;
94  import org.mycore.datamodel.classifications2.MCRCategoryDAOFactory;
95  import org.mycore.datamodel.classifications2.MCRCategoryID;
96  import org.mycore.datamodel.classifications2.utils.MCRCategoryTransformer;
97  import org.mycore.datamodel.common.MCRAbstractMetadataVersion;
98  import org.mycore.datamodel.common.MCRDataURL;
99  import org.mycore.datamodel.common.MCRXMLMetadataManager;
100 import org.mycore.datamodel.metadata.MCRDerivate;
101 import org.mycore.datamodel.metadata.MCRFileMetadata;
102 import org.mycore.datamodel.metadata.MCRMetadataManager;
103 import org.mycore.datamodel.metadata.MCRObjectDerivate;
104 import org.mycore.datamodel.metadata.MCRObjectID;
105 import org.mycore.datamodel.niofs.MCRPath;
106 import org.mycore.datamodel.niofs.MCRPathXML;
107 import org.mycore.services.http.MCRHttpUtils;
108 import org.mycore.services.i18n.MCRTranslation;
109 import org.mycore.tools.MCRObjectFactory;
110 import org.xml.sax.InputSource;
111 import org.xml.sax.SAXException;
112 import org.xml.sax.XMLReader;
113 
114 import jakarta.servlet.ServletContext;
115 
116 /**
117  * Reads XML documents from various URI types. This resolver is used to read DTDs, XML Schema files, XSL document()
118  * usages, xsl:include usages and MyCoRe Editor include declarations. DTDs and Schema files are read from the CLASSPATH
119  * of the application when XML is parsed. XML document() calls and xsl:include calls within XSL stylesheets can be read
120  * from URIs of type resource, webapp, file, session, query or mcrobject. MyCoRe editor include declarations can read
121  * XML files from resource, webapp, file, session, http or https, query, or mcrobject URIs.
122  *
123  * @author Frank L\u00FCtzenkirchen
124  * @author Thomas Scheffler (yagee)
125  */
126 public final class MCRURIResolver implements URIResolver {
127     static final Logger LOGGER = LogManager.getLogger(MCRURIResolver.class);
128 
129     static final String SESSION_OBJECT_NAME = "URI_RESOLVER_DEBUG";
130 
131     private static final String CONFIG_PREFIX = "MCR.URIResolver.";
132 
133     private static Map<String, URIResolver> SUPPORTED_SCHEMES;
134 
135     private static MCRResolverProvider EXT_RESOLVER;
136 
137     private static MCRURIResolver singleton;
138 
139     private static ServletContext context;
140 
141     static {
142         try {
143             EXT_RESOLVER = getExternalResolverProvider();
144             singleton = new MCRURIResolver();
145         } catch (Exception exc) {
146             LOGGER.error("Unable to initialize MCRURIResolver", exc);
147         }
148     }
149 
150     /**
151      * Creates a new MCRURIResolver
152      */
153     private MCRURIResolver() {
154         SUPPORTED_SCHEMES = Collections.unmodifiableMap(getResolverMapping());
155     }
156 
157     private static MCRResolverProvider getExternalResolverProvider() {
158         return MCRConfiguration2.getClass(CONFIG_PREFIX + "ExternalResolver.Class")
159             .map(c -> {
160                 try {
161                     return (MCRResolverProvider) c.getDeclaredConstructor().newInstance();
162                 } catch (ReflectiveOperationException e) {
163                     LOGGER.warn("Could not instantiate external Resolver class", e);
164                     return null;
165                 }
166             }).orElse(HashMap::new);
167     }
168 
169     /**
170      * Returns the MCRURIResolver singleton
171      */
172     public static MCRURIResolver instance() {
173         return singleton;
174     }
175 
176     /**
177      * Initializes the MCRURIResolver for servlet applications.
178      *
179      * @param ctx
180      *            the servlet context of this web application
181      */
182     public static synchronized void init(ServletContext ctx) {
183         context = ctx;
184     }
185 
186     public static Hashtable<String, String> getParameterMap(String key) {
187         String[] param;
188         StringTokenizer tok = new StringTokenizer(key, "&");
189         Hashtable<String, String> params = new Hashtable<>();
190 
191         while (tok.hasMoreTokens()) {
192             param = tok.nextToken().split("=");
193             params.put(param[0], param.length >= 2 ? param[1] : "");
194         }
195         return params;
196     }
197 
198     static URI resolveURI(String href, String base) {
199         return Optional.ofNullable(base)
200             .map(URI::create)
201             .map(u -> u.resolve(href))
202             .orElse(URI.create(href));
203     }
204 
205     public static ServletContext getServletContext() {
206         return context;
207     }
208 
209     private HashMap<String, URIResolver> getResolverMapping() {
210         final Map<String, URIResolver> extResolverMapping = EXT_RESOLVER.getURIResolverMapping();
211         extResolverMapping.putAll(new MCRModuleResolverProvider().getURIResolverMapping());
212         // set Map to final size with loadfactor: full
213         HashMap<String, URIResolver> supportedSchemes = new HashMap<>(10 + extResolverMapping.size(), 1);
214         // don't let interal mapping be overwritten
215         supportedSchemes.putAll(extResolverMapping);
216         supportedSchemes.put("webapp", new MCRWebAppResolver());
217         supportedSchemes.put("ifs", new MCRIFSResolver());
218         supportedSchemes.put("mcrfile", new MCRMCRFileResolver());
219         supportedSchemes.put("mcrobject", new MCRObjectResolver());
220         supportedSchemes.put("session", new MCRSessionResolver());
221         supportedSchemes.put("access", new MCRACLResolver());
222         supportedSchemes.put("resource", new MCRResourceResolver());
223         supportedSchemes.put("localclass", new MCRLocalClassResolver());
224         supportedSchemes.put("classification", new MCRClassificationResolver());
225         supportedSchemes.put("buildxml", new MCRBuildXMLResolver());
226         supportedSchemes.put("catchEx", new MCRExceptionAsXMLResolver());
227         supportedSchemes.put("notnull", new MCRNotNullResolver());
228         supportedSchemes.put("xslStyle", new MCRXslStyleResolver());
229         supportedSchemes.put("xslTransform", new MCRLayoutTransformerResolver());
230         supportedSchemes.put("xslInclude", new MCRXslIncludeResolver());
231         supportedSchemes.put("xslImport", new MCRXslImportResolver());
232         supportedSchemes.put("versioninfo", new MCRVersionInfoResolver());
233         supportedSchemes.put("deletedMcrObject", new MCRDeletedObjectResolver());
234         supportedSchemes.put("fileMeta", new MCRFileMetadataResolver());
235         supportedSchemes.put("basket", new org.mycore.frontend.basket.MCRBasketResolver());
236         supportedSchemes.put("language", new org.mycore.datamodel.language.MCRLanguageResolver());
237         supportedSchemes.put("chooseTemplate", new MCRChooseTemplateResolver());
238         supportedSchemes.put("redirect", new MCRRedirectResolver());
239         supportedSchemes.put("data", new MCRDataURLResolver());
240         supportedSchemes.put("i18n", new MCRI18NResolver());
241         supportedSchemes.put("checkPermissionChain", new MCRCheckPermissionChainResolver());
242         supportedSchemes.put("checkPermission", new MCRCheckPermissionResolver());
243         MCRRESTResolver restResolver = new MCRRESTResolver();
244         supportedSchemes.put("http", restResolver);
245         supportedSchemes.put("https", restResolver);
246         supportedSchemes.put("file", new MCRFileResolver());
247         supportedSchemes.put("cache", new MCRCachingResolver());
248         return supportedSchemes;
249     }
250 
251     /**
252      * URI Resolver that resolves XSL document() or xsl:include calls.
253      *
254      * @see javax.xml.transform.URIResolver
255      */
256     @Override
257     public Source resolve(String href, String base) throws TransformerException {
258         if (LOGGER.isDebugEnabled()) {
259             if (base != null) {
260                 LOGGER.debug("Including {} from {}", href, base);
261                 addDebugInfo(href, base);
262             } else {
263                 LOGGER.debug("Including {}", href);
264                 addDebugInfo(href, null);
265             }
266         }
267         if (!href.contains(":")) {
268             return tryResolveXSL(href, base);
269         }
270 
271         String scheme = getScheme(href, base);
272 
273         URIResolver uriResolver = SUPPORTED_SCHEMES.get(scheme);
274         if (uriResolver != null) {
275             return uriResolver.resolve(href, base);
276         } else { // try to handle as URL, use default resolver for file:// and
277             try {
278                 InputSource entity = MCREntityResolver.instance().resolveEntity(null, href);
279                 if (entity != null) {
280                     LOGGER.debug("Resolved via EntityResolver: {}", entity.getSystemId());
281                     return new MCRLazyStreamSource(entity::getByteStream, entity.getSystemId());
282                 }
283             } catch (IOException e) {
284                 LOGGER.debug("Error while resolving uri: {}", href);
285             }
286             // http://
287             if (href.endsWith("/") && scheme.equals("file")) {
288                 //cannot stream directories
289                 return null;
290             }
291             StreamSource streamSource = new StreamSource();
292             streamSource.setSystemId(href);
293             return streamSource;
294         }
295     }
296 
297     private Source tryResolveXSL(String href, String base) throws TransformerException {
298         if (href.endsWith(".xsl")) {
299             final String uri = "resource:xsl/" + href;
300             LOGGER.debug("Trying to resolve {} from uri {}", href, uri);
301             return SUPPORTED_SCHEMES.get("resource").resolve(uri, base);
302         }
303         return null;
304     }
305 
306     private void addDebugInfo(String href, String base) {
307         MCRURIResolverFilter.uriList.get().add(href + " from " + base);
308     }
309 
310     /**
311      * Reads XML from URIs of various type.
312      *
313      * @param uri
314      *            the URI where to read the XML from
315      * @return the root element of the XML document
316      */
317     public Element resolve(String uri) {
318         if (LOGGER.isDebugEnabled()) {
319             addDebugInfo(uri, "JAVA method invocation");
320         }
321         MCRSourceContent content;
322         try {
323             content = MCRSourceContent.getInstance(uri);
324             return content == null ? null : content.asXML().getRootElement().detach();
325         } catch (Exception e) {
326             throw new MCRException("Error while resolving " + uri, e);
327         }
328     }
329 
330     /**
331      * Returns the protocol or scheme for the given URI.
332      *
333      * @param uri
334      *            the URI to parse
335      * @param base
336      *            if uri is relative, resolve scheme from base parameter
337      * @return the protocol/scheme part before the ":"
338      */
339     public String getScheme(String uri, String base) {
340         StringTokenizer uriTokenizer = new StringTokenizer(uri, ":");
341         if (uriTokenizer.hasMoreTokens()) {
342             return uriTokenizer.nextToken();
343         }
344         if (base != null) {
345             uriTokenizer = new StringTokenizer(base, ":");
346             if (uriTokenizer.hasMoreTokens()) {
347                 return uriTokenizer.nextToken();
348             }
349         }
350         return null;
351     }
352 
353     URIResolver getResolver(String scheme) {
354         if (SUPPORTED_SCHEMES.containsKey(scheme)) {
355             return SUPPORTED_SCHEMES.get(scheme);
356         }
357         String msg = "Unsupported scheme type: " + scheme;
358         throw new MCRUsageException(msg);
359     }
360 
361     /**
362      * Reads xml from an InputStream and returns the parsed root element.
363      *
364      * @param in
365      *            the InputStream that contains the XML document
366      * @return the root element of the parsed input stream
367      */
368     protected Element parseStream(InputStream in) throws JDOMException, IOException {
369         SAXBuilder builder = new SAXBuilder(XMLReaders.NONVALIDATING);
370         builder.setEntityResolver(MCREntityResolver.instance());
371 
372         return builder.build(in).getRootElement();
373     }
374 
375     /**
376      * provides a URI -- Resolver Mapping One can implement this interface to provide additional URI schemes this
377      * MCRURIResolver should handle, too. To add your mapping you have to set the
378      * <code>MCR.URIResolver.ExternalResolver.Class</code> property to the implementing class.
379      *
380      * @author Thomas Scheffler
381      */
382     public interface MCRResolverProvider {
383         /**
384          * provides a Map of URIResolver mappings. Key is the scheme, e.g. <code>http</code>, where value is an
385          * implementation of {@link URIResolver}.
386          *
387          * @see URIResolver
388          * @return a Map of URIResolver mappings
389          */
390         Map<String, URIResolver> getURIResolverMapping();
391     }
392 
393     public interface MCRXslIncludeHrefs {
394         List<String> getHrefs();
395     }
396 
397     private static class MCRModuleResolverProvider implements MCRResolverProvider {
398         private final Map<String, URIResolver> resolverMap = new HashMap<>();
399 
400         MCRModuleResolverProvider() {
401             MCRConfiguration2.getSubPropertiesMap(CONFIG_PREFIX + "ModuleResolver.")
402                 .forEach(this::registerUriResolver);
403         }
404 
405         @Override
406         public Map<String, URIResolver> getURIResolverMapping() {
407             return resolverMap;
408         }
409 
410         private void registerUriResolver(String scheme, String className) {
411             try {
412                 resolverMap.put(scheme, MCRConfiguration2.instantiateClass(className));
413             } catch (RuntimeException re) {
414                 throw new MCRException("Cannot instantiate " + className + " for URI scheme " + scheme, re);
415             }
416         }
417 
418     }
419 
420     private static class MCRFileResolver implements URIResolver {
421 
422         @Override
423         public Source resolve(String href, String base) throws TransformerException {
424             URI hrefURI = MCRURIResolver.resolveURI(href, base);
425             if (!hrefURI.getScheme().equals("file")) {
426                 throw new TransformerException("Unsupport file uri scheme: " + hrefURI.getScheme());
427             }
428             Path path = Paths.get(hrefURI);
429             StreamSource source;
430             try {
431                 source = new StreamSource(Files.newInputStream(path), hrefURI.toASCIIString());
432                 return source;
433             } catch (IOException e) {
434                 throw new TransformerException(e);
435             }
436         }
437     }
438 
439     private static class MCRRESTResolver implements URIResolver {
440         private static final long MAX_OBJECT_SIZE = MCRConfiguration2.getLong(CONFIG_PREFIX + "REST.MaxObjectSize")
441             .orElse(128 * 1024l);
442 
443         private static final int MAX_CACHE_ENTRIES = MCRConfiguration2.getInt(CONFIG_PREFIX + "REST.MaxCacheEntries")
444             .orElse(1000);
445 
446         private static final int REQUEST_TIMEOUT = MCRConfiguration2.getInt(CONFIG_PREFIX + "REST.RequestTimeout")
447             .orElse(30000);
448 
449         private CloseableHttpClient restClient;
450 
451         private org.apache.logging.log4j.Logger logger;
452 
453         MCRRESTResolver() {
454             CacheConfig cacheConfig = CacheConfig.custom()
455                 .setMaxObjectSize(MAX_OBJECT_SIZE)
456                 .setMaxCacheEntries(MAX_CACHE_ENTRIES)
457                 .build();
458             RequestConfig requestConfig = RequestConfig.custom()
459                 .setConnectTimeout(REQUEST_TIMEOUT)
460                 .setSocketTimeout(REQUEST_TIMEOUT)
461                 .build();
462             this.restClient = CachingHttpClients.custom()
463                 .setCacheConfig(cacheConfig)
464                 .setDefaultRequestConfig(requestConfig)
465                 .setUserAgent(MCRHttpUtils.getHttpUserAgent())
466                 .useSystemProperties()
467                 .build();
468             MCRShutdownHandler.getInstance().addCloseable(this::close);
469             this.logger = LogManager.getLogger();
470         }
471 
472         public void close() {
473             try {
474                 restClient.close();
475             } catch (IOException e) {
476                 LogManager.getLogger().warn("Exception while closing http client.", e);
477             }
478         }
479 
480         @Override
481         public Source resolve(String href, String base) throws TransformerException {
482             URI hrefURI = MCRURIResolver.resolveURI(href, base);
483             try {
484                 HttpCacheContext context = HttpCacheContext.create();
485                 HttpGet get = new HttpGet(hrefURI);
486                 CloseableHttpResponse response = restClient.execute(get, context);
487                 logger.debug(() -> getCacheDebugMsg(hrefURI, context));
488                 try (InputStream content = response.getEntity().getContent()) {
489                     final Source source = new MCRStreamContent(content).getReusableCopy().getSource();
490                     source.setSystemId(hrefURI.toASCIIString());
491                     return source;
492                 } finally {
493                     response.close();
494                     get.reset();
495                 }
496             } catch (IOException e) {
497                 throw new TransformerException(e);
498             }
499         }
500 
501         private String getCacheDebugMsg(URI hrefURI, HttpCacheContext context) {
502             String msg = hrefURI.toASCIIString() + ": ";
503             switch (context.getCacheResponseStatus()) {
504             case CACHE_HIT:
505                 msg += "A response was generated from the cache with " +
506                     "no requests sent upstream";
507                 break;
508             case CACHE_MODULE_RESPONSE:
509                 msg += "The response was generated directly by the " +
510                     "caching module";
511                 break;
512             case CACHE_MISS:
513                 msg += "The response came from an upstream server";
514                 break;
515             case VALIDATED:
516                 msg += "The response was generated from the cache " +
517                     "after validating the entry with the origin server";
518                 break;
519             }
520             return msg;
521         }
522 
523     }
524 
525     private static class MCRObjectResolver implements URIResolver {
526 
527         /**
528          * Reads local MCRObject with a given ID from the store.
529          *
530          * @param href
531          *            for example, "mcrobject:DocPortal_document_07910401"
532          * @returns XML representation from MCRXMLContainer
533          */
534         @Override
535         public Source resolve(String href, String base) throws TransformerException {
536             String id = href.substring(href.indexOf(":") + 1);
537             LOGGER.debug("Reading MCRObject with ID {}", id);
538             Map<String, String> params;
539             StringTokenizer tok = new StringTokenizer(id, "?");
540             id = tok.nextToken();
541 
542             if (tok.hasMoreTokens()) {
543                 params = getParameterMap(tok.nextToken());
544             } else {
545                 params = Collections.emptyMap();
546             }
547 
548             MCRObjectID mcrid = MCRObjectID.getInstance(id);
549             try {
550                 MCRXMLMetadataManager xmlmm = MCRXMLMetadataManager.instance();
551                 MCRContent content = params.containsKey("r")
552                     ? xmlmm.retrieveContent(mcrid, params.get("r"))
553                     : xmlmm.retrieveContent(mcrid);
554                 if (content == null) {
555                     return null;
556                 }
557                 LOGGER.debug("end resolving {}", href);
558                 return content.getSource();
559             } catch (IOException e) {
560                 throw new TransformerException(e);
561             }
562         }
563 
564     }
565 
566     /**
567      * Reads XML from a static file within the web application. the URI in the format webapp:path/to/servlet
568      */
569     private static class MCRWebAppResolver implements URIResolver {
570 
571         @Override
572         public Source resolve(String href, String base) throws TransformerException {
573             String path = href.substring(href.indexOf(":") + 1);
574             if (path.charAt(0) != '/') {
575                 path = '/' + path;
576             }
577 
578             if (MCRDeveloperTools.overrideActive()) {
579                 final Optional<Path> overriddenFilePath = MCRDeveloperTools.getOverriddenFilePath(path, true);
580                 if (overriddenFilePath.isPresent()) {
581                     return new StreamSource(overriddenFilePath.get().toFile());
582                 }
583             }
584 
585             LOGGER.debug("Reading xml from webapp {}", path);
586             try {
587                 URL resource = context.getResource(path);
588                 if (resource != null) {
589                     return new StreamSource(resource.toURI().toASCIIString());
590                 }
591             } catch (Exception ex) {
592                 throw new TransformerException(ex);
593             }
594             LOGGER.error("File does not exist: {}", context.getRealPath(path));
595             throw new TransformerException("Could not find web resource: " + path);
596         }
597     }
598 
599     private static class MCRChooseTemplateResolver implements URIResolver {
600 
601         private static Document getStylesheets(List<String> temps) {
602 
603             Element rootOut = new Element("stylesheet", MCRConstants.XSL_NAMESPACE).setAttribute("version", "1.0");
604             Document jdom = new Document(rootOut);
605 
606             if (temps.isEmpty()) {
607                 return jdom;
608             }
609 
610             for (String templateName : temps) {
611                 rootOut.addContent(
612                     new Element("include", MCRConstants.XSL_NAMESPACE).setAttribute("href", templateName + ".xsl"));
613             }
614 
615             // first template named "chooseTemplate" in chooseTemplate.xsl
616             Element template = new Element("template", MCRConstants.XSL_NAMESPACE).setAttribute("name",
617                 "chooseTemplate");
618             Element choose = new Element("choose", MCRConstants.XSL_NAMESPACE);
619             // second template named "get.templates" in chooseTemplate.xsl
620             Element template2 = new Element("template", MCRConstants.XSL_NAMESPACE).setAttribute("name",
621                 "get.templates");
622             Element templates = new Element("templates");
623 
624             for (String templateName : temps) {
625                 // add elements in the first template
626                 Element when = new Element("when", MCRConstants.XSL_NAMESPACE).setAttribute("test",
627                     "$template = '" + templateName + "'");
628                 when.addContent(
629                     new Element("call-template", MCRConstants.XSL_NAMESPACE).setAttribute("name", templateName));
630                 choose.addContent(when);
631 
632                 // add elements in the second template
633                 templates.addContent(new Element("template").setAttribute("category", "master").setText(templateName));
634             }
635 
636             // first
637             template.addContent(choose);
638             rootOut.addContent(template);
639             // second
640             template2.addContent(templates);
641             rootOut.addContent(template2);
642             return jdom;
643         }
644 
645         @Override
646         public Source resolve(String href, String base) throws TransformerException {
647             String type = href.substring(href.indexOf(":") + 1);
648             String path = "/templates/" + type + "/";
649             LOGGER.debug("Reading templates from {}", path);
650             Set<String> resourcePaths = context.getResourcePaths(path);
651             ArrayList<String> templates = new ArrayList<>();
652             if (resourcePaths != null) {
653                 for (String resourcePath : resourcePaths) {
654                     if (!resourcePath.endsWith("/")) {
655                         //only handle directories
656                         continue;
657                     }
658                     String templateName = resourcePath.substring(path.length(), resourcePath.length() - 1);
659                     LOGGER.debug("Checking if template: {}", templateName);
660                     if (templateName.contains("/")) {
661                         continue;
662                     }
663                     templates.add(templateName);
664                 }
665                 Collections.sort(templates);
666             }
667             LOGGER.info("Found theses templates: {}", templates);
668             return new JDOMSource(getStylesheets(templates));
669         }
670 
671     }
672 
673     /**
674      * Reads XML from the CLASSPATH of the application. the location of the file in the format resource:path/to/file
675      */
676     private static class MCRResourceResolver implements URIResolver {
677 
678         @Override
679         public Source resolve(String href, String base) throws TransformerException {
680             String path = href.substring(href.indexOf(":") + 1);
681             URL resource = MCRConfigurationDir.getConfigResource(path);
682             if (resource != null) {
683                 //have to use SAX here to resolve entities
684                 if (path.endsWith(".xsl")) {
685                     XMLReader reader;
686                     try {
687                         reader = MCRXMLParserFactory.getNonValidatingParser().getXMLReader();
688                     } catch (SAXException | ParserConfigurationException e) {
689                         throw new TransformerException(e);
690                     }
691                     reader.setEntityResolver(MCREntityResolver.instance());
692                     InputSource input = new InputSource(resource.toString());
693                     SAXSource saxSource = new SAXSource(reader, input);
694                     LOGGER.debug("include stylesheet: {}", saxSource.getSystemId());
695                     return saxSource;
696                 }
697                 return MCRURIResolver.instance().resolve(resource.toString(), base);
698             }
699             return null;
700         }
701     }
702 
703     /**
704      * Delivers a jdom Element created by any local class that implements URIResolver
705      * interface. the class name of the file in the format localclass:org.mycore.ClassName?mode=getAll
706      */
707     private static class MCRLocalClassResolver implements URIResolver {
708 
709         @Override
710         public Source resolve(String href, String base) throws TransformerException {
711             String classname = href.substring(href.indexOf(":") + 1, href.indexOf("?"));
712             Class<? extends URIResolver> cl = null;
713             LogManager.getLogger(this.getClass()).debug("Loading Class: {}", classname);
714             URIResolver resolver;
715             try {
716                 cl = MCRClassTools.forName(classname);
717                 resolver = cl.getDeclaredConstructor().newInstance();
718             } catch (Exception e) {
719                 throw new TransformerException(e);
720             }
721             return resolver.resolve(href, base);
722         }
723 
724     }
725 
726     private static class MCRSessionResolver implements URIResolver {
727 
728         /**
729          * Reads XML from URIs of type session:key. The method MCRSession.get( key ) is called and must return a JDOM
730          * element.
731          *
732          * @see org.mycore.common.MCRSession#get(Object)
733          * @param href
734          *            the URI in the format session:key
735          * @return the root element of the xml document
736          */
737         @Override
738         public Source resolve(String href, String base) throws TransformerException {
739             String key = href.substring(href.indexOf(":") + 1);
740             LOGGER.debug("Reading xml from session using key {}", key);
741             Element value = (Element) MCRSessionMgr.getCurrentSession().get(key);
742             return new JDOMSource(value.clone());
743         }
744 
745     }
746 
747     private static class MCRIFSResolver implements URIResolver {
748 
749         /**
750          * Reads XML from a http or https URL.
751          *
752          * @param href
753          *            the URL of the xml document
754          * @return the root element of the xml document
755          */
756         @Override
757         public Source resolve(String href, String base) throws TransformerException {
758             LOGGER.debug("Reading xml from url {}", href);
759 
760             String path = href.substring(href.indexOf(":") + 1);
761 
762             int i = path.indexOf("?host");
763             if (i > 0) {
764                 path = path.substring(0, i);
765             }
766             StringTokenizer st = new StringTokenizer(path, "/");
767 
768             String ownerID = st.nextToken();
769             try {
770                 String aPath = MCRXMLFunctions.decodeURIPath(path.substring(ownerID.length() + 1));
771                 // TODO: make this more pretty
772                 if (ownerID.endsWith(":")) {
773                     ownerID = ownerID.substring(0, ownerID.length() - 1);
774                 }
775                 LOGGER.debug("Get {} path: {}", ownerID, aPath);
776                 return new JDOMSource(MCRPathXML.getDirectoryXML(MCRPath.getPath(ownerID, aPath)));
777             } catch (IOException | URISyntaxException e) {
778                 throw new TransformerException(e);
779             }
780         }
781     }
782 
783     private static class MCRMCRFileResolver implements URIResolver {
784         @Override
785         public Source resolve(String href, String base) throws TransformerException {
786             LOGGER.debug("Reading xml from MCRFile {}", href);
787             MCRPath file = null;
788             String id = href.substring(href.indexOf(":") + 1);
789             if (id.contains("/")) {
790                 // assume thats a derivate with path
791                 try {
792                     MCRObjectID derivateID = MCRObjectID.getInstance(id.substring(0, id.indexOf("/")));
793                     String path = id.substring(id.indexOf("/"));
794                     file = MCRPath.getPath(derivateID.toString(), path);
795                 } catch (MCRException exc) {
796                     // just check if the id is valid, don't care about the exception
797                 }
798             }
799             if (file == null) {
800                 throw new TransformerException("mcrfile: Resolver needs a path: " + href);
801             }
802             try {
803                 return new MCRPathContent(file).getSource();
804             } catch (Exception e) {
805                 throw new TransformerException(e);
806             }
807         }
808     }
809 
810     private static class MCRACLResolver implements URIResolver {
811 
812         private static final String ACTION_PARAM = "action";
813 
814         private static final String OBJECT_ID_PARAM = "object";
815 
816         /**
817          * Returns access controll rules as XML
818          */
819         @Override
820         public Source resolve(String href, String base) throws TransformerException {
821             String key = href.substring(href.indexOf(":") + 1);
822             LOGGER.debug("Reading xml from query result using key :{}", key);
823 
824             String[] param;
825             StringTokenizer tok = new StringTokenizer(key, "&");
826             Hashtable<String, String> params = new Hashtable<>();
827 
828             while (tok.hasMoreTokens()) {
829                 param = tok.nextToken().split("=");
830                 params.put(param[0], param[1]);
831             }
832 
833             String action = params.get(ACTION_PARAM);
834             String objId = params.get(OBJECT_ID_PARAM);
835 
836             if (action == null || objId == null) {
837                 return null;
838             }
839 
840             Element container = new Element("servacls").setAttribute("class", "MCRMetaAccessRule");
841 
842             if (action.equals("all")) {
843                 for (String permission : MCRAccessManager.getPermissionsForID(objId)) {
844                     // one pool Element under access per defined AccessRule in
845                     // Pool
846                     // for (Object-)ID
847                     addRule(container, permission, MCRAccessManager.requireRulesInterface().getRule(objId, permission));
848                 }
849             } else {
850                 addRule(container, action, MCRAccessManager.requireRulesInterface().getRule(objId, action));
851             }
852 
853             return new JDOMSource(container);
854         }
855 
856         private void addRule(Element root, String pool, Element rule) {
857             if (rule != null && pool != null) {
858                 Element poolElement = new Element("servacl").setAttribute("permission", pool);
859                 poolElement.addContent(rule);
860                 root.addContent(poolElement);
861             }
862         }
863 
864     }
865 
866     private static class MCRClassificationResolver implements URIResolver {
867 
868         private static final Pattern EDITORFORMAT_PATTERN = Pattern.compile("(\\[)([^\\]]*)(\\])");
869 
870         private static final String FORMAT_CONFIG_PREFIX = CONFIG_PREFIX + "Classification.Format.";
871 
872         private static final String SORT_CONFIG_PREFIX = CONFIG_PREFIX + "Classification.Sort.";
873 
874         private static MCRCache<String, Element> categoryCache;
875 
876         private static MCRCategoryDAO DAO;
877 
878         static {
879             try {
880                 DAO = MCRCategoryDAOFactory.getInstance();
881                 categoryCache = new MCRCache<>(
882                     MCRConfiguration2.getInt(CONFIG_PREFIX + "Classification.CacheSize").orElse(1000),
883                     "URIResolver categories");
884             } catch (Exception exc) {
885                 LOGGER.error("Unable to initialize classification resolver", exc);
886             }
887         }
888 
889         MCRClassificationResolver() {
890         }
891 
892         private static String getLabelFormat(String editorString) {
893             Matcher m = EDITORFORMAT_PATTERN.matcher(editorString);
894             if (m.find() && m.groupCount() == 3) {
895                 String formatDef = m.group(2);
896                 return MCRConfiguration2.getStringOrThrow(FORMAT_CONFIG_PREFIX + formatDef);
897             }
898             return null;
899         }
900 
901         private static boolean shouldSortCategories(String classId) {
902             return MCRConfiguration2.getBoolean(SORT_CONFIG_PREFIX + classId).orElse(true);
903         }
904 
905         private static long getSystemLastModified() {
906             long xmlLastModified = MCRXMLMetadataManager.instance().getLastModified();
907             long classLastModified = DAO.getLastModified();
908             return Math.max(xmlLastModified, classLastModified);
909         }
910 
911         /**
912          * returns a classification in a specific format. Syntax:
913          * <code>classification:{editor[Complete]['['formatAlias']']|metadata}:{Levels}[:noEmptyLeaves]:{parents|
914          * children}:{ClassID}[:CategID] formatAlias: MCRConfiguration property
915          * MCR.UURResolver.Classification.Format.FormatAlias
916          *
917          * @param href
918          *            URI in the syntax above
919          * @return the root element of the XML document
920          * @see MCRCategoryTransformer
921          */
922         @Override
923         public Source resolve(String href, String base) throws TransformerException {
924             LOGGER.debug("start resolving {}", href);
925             String cacheKey = getCacheKey(href);
926             Element returns = categoryCache.getIfUpToDate(cacheKey, getSystemLastModified());
927             if (returns == null) {
928                 returns = getClassElement(href);
929                 if (returns != null) {
930                     categoryCache.put(cacheKey, returns);
931                 }
932             }
933             return new JDOMSource(returns);
934         }
935 
936         protected String getCacheKey(String uri) {
937             return uri;
938         }
939 
940         private Element getClassElement(String uri) {
941             StringTokenizer pst = new StringTokenizer(uri, ":", true);
942             if (pst.countTokens() < 9) {
943                 // sanity check
944                 throw new IllegalArgumentException("Invalid format of uri for retrieval of classification: " + uri);
945             }
946 
947             pst.nextToken(); // "classification"
948             pst.nextToken(); // :
949             String format = pst.nextToken();
950             pst.nextToken(); // :
951 
952             String levelS = pst.nextToken();
953             pst.nextToken(); // :
954             int levels = "all".equals(levelS) ? -1 : Integer.parseInt(levelS);
955 
956             String axis;
957             String token = pst.nextToken();
958             pst.nextToken(); // :
959             boolean emptyLeaves = !"noEmptyLeaves".equals(token);
960             if (!emptyLeaves) {
961                 axis = pst.nextToken();
962                 pst.nextToken(); // :
963             } else {
964                 axis = token;
965             }
966 
967             String classID = pst.nextToken();
968             StringBuilder categID = new StringBuilder();
969             if (pst.hasMoreTokens()) {
970                 pst.nextToken(); // :
971                 while (pst.hasMoreTokens()) {
972                     categID.append(pst.nextToken());
973                 }
974             }
975 
976             String categ;
977             try {
978                 categ = MCRXMLFunctions.decodeURIPath(categID.toString());
979             } catch (URISyntaxException e) {
980                 categ = categID.toString();
981             }
982             MCRCategory cl = null;
983             LOGGER.debug("categoryCache entry invalid or not found: start MCRClassificationQuery");
984             if (axis.equals("children")) {
985                 if (categ.length() > 0) {
986                     cl = DAO.getCategory(new MCRCategoryID(classID, categ), levels);
987                 } else {
988                     cl = DAO.getCategory(MCRCategoryID.rootID(classID), levels);
989                 }
990             } else if (axis.equals("parents")) {
991                 if (categ.length() == 0) {
992                     LOGGER.error("Cannot resolve parent axis without a CategID. URI: {}", uri);
993                     throw new IllegalArgumentException(
994                         "Invalid format (categID is required in mode 'parents') "
995                             + "of uri for retrieval of classification: "
996                             + uri);
997                 }
998                 cl = DAO.getRootCategory(new MCRCategoryID(classID, categ), levels);
999             }
1000             if (cl == null) {
1001                 return null;
1002             }
1003 
1004             Element returns;
1005             LOGGER.debug("start transformation of ClassificationQuery");
1006             if (format.startsWith("editor")) {
1007                 boolean completeId = format.startsWith("editorComplete");
1008                 boolean sort = shouldSortCategories(classID);
1009                 String labelFormat = getLabelFormat(format);
1010                 if (labelFormat == null) {
1011                     returns = MCRCategoryTransformer.getEditorItems(cl, sort, emptyLeaves, completeId);
1012                 } else {
1013                     returns = MCRCategoryTransformer.getEditorItems(cl, labelFormat, sort, emptyLeaves, completeId);
1014                 }
1015             } else if (format.equals("metadata")) {
1016                 returns = MCRCategoryTransformer.getMetaDataDocument(cl, false).getRootElement().detach();
1017             } else {
1018                 LOGGER.error("Unknown target format given. URI: {}", uri);
1019                 throw new IllegalArgumentException(
1020                     "Invalid target format (" + format + ") in uri for retrieval of classification: " + uri);
1021             }
1022             LOGGER.debug("end resolving {}", uri);
1023             return returns;
1024         }
1025 
1026     }
1027 
1028     private static class MCRExceptionAsXMLResolver implements URIResolver {
1029 
1030         @Override
1031         public Source resolve(String href, String base) throws TransformerException {
1032             String target = href.substring(href.indexOf(":") + 1);
1033 
1034             try {
1035                 return MCRURIResolver.instance().resolve(target, base);
1036             } catch (Exception ex) {
1037                 LOGGER.debug("Caught {}. Put it into XML to process in XSL!", ex.getClass().getName());
1038                 Element exception = new Element("exception");
1039                 Element message = new Element("message");
1040                 Element stacktraceElement = new Element("stacktrace");
1041 
1042                 stacktraceElement.setAttribute("space", "preserve", Namespace.XML_NAMESPACE);
1043 
1044                 exception.addContent(message);
1045                 exception.addContent(stacktraceElement);
1046 
1047                 message.setText(ex.getMessage());
1048 
1049                 try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
1050                     ex.printStackTrace(pw);
1051                     stacktraceElement.setText(pw.toString());
1052                 } catch (IOException e) {
1053                     throw new MCRException("Error while writing Exception to String!", e);
1054                 }
1055 
1056                 return new JDOMSource(exception);
1057             }
1058         }
1059     }
1060 
1061     /**
1062      * Ensures that the return of the given uri is never null. When the return is null, or the uri throws an exception,
1063      * this resolver will return an empty XML element instead. Usage: notnull:<anyMyCoReURI>
1064      */
1065     private static class MCRNotNullResolver implements URIResolver {
1066 
1067         @Override
1068         public Source resolve(String href, String base) throws TransformerException {
1069             String target = href.substring(href.indexOf(":") + 1);
1070             // fixes exceptions if suburi is empty like "mcrobject:"
1071             String subUri = target.substring(target.indexOf(":") + 1);
1072             if (subUri.length() == 0) {
1073                 return new JDOMSource(new Element("null"));
1074             }
1075             // end fix
1076             LOGGER.debug("Ensuring xml is not null: {}", target);
1077             try {
1078                 Source result = MCRURIResolver.instance().resolve(target, base);
1079                 if (result != null) {
1080                     // perform actual construction of xml document, as in MCRURIResolver#resolve(String),
1081                     // by performing the same actions as MCRSourceContent#asXml(),
1082                     // but with a MCRXMLParser configured to be silent to suppress undesirable log messages
1083                     MCRContent content = new MCRSourceContent(result).getBaseContent();
1084                     Document document = MCRXMLParserFactory.getParser(false, true).parseXML(content);
1085                     return new JDOMSource(document.getRootElement().detach());
1086                 } else {
1087                     LOGGER.debug("MCRNotNullResolver returning empty xml");
1088                     return new JDOMSource(new Element("null"));
1089                 }
1090             } catch (Exception ex) {
1091                 LOGGER.info("MCRNotNullResolver caught exception: {}", ex.getLocalizedMessage());
1092                 LOGGER.debug(ex.getStackTrace());
1093                 LOGGER.debug("MCRNotNullResolver returning empty xml");
1094                 return new JDOMSource(new Element("null"));
1095             }
1096         }
1097     }
1098 
1099     /**
1100      * Transform result of other resolver with stylesheet. Usage: xslStyle:<stylesheet><,stylesheet><?param1=value1
1101      * <&param2=value2>>:<anyMyCoReURI> To <stylesheet> is extension .xsl added. File is searched in classpath.
1102      */
1103     private static class MCRXslStyleResolver implements URIResolver {
1104 
1105         @Override
1106         public Source resolve(String href, String base) throws TransformerException {
1107             String help = href.substring(href.indexOf(":") + 1);
1108             String stylesheets = new StringTokenizer(help, ":").nextToken();
1109             String target = help.substring(help.indexOf(":") + 1);
1110 
1111             String subUri = target.substring(target.indexOf(":") + 1);
1112             if (subUri.length() == 0) {
1113                 return new JDOMSource(new Element("null"));
1114             }
1115 
1116             Map<String, String> params;
1117             StringTokenizer tok = new StringTokenizer(stylesheets, "?");
1118             stylesheets = tok.nextToken();
1119 
1120             if (tok.hasMoreTokens()) {
1121                 params = getParameterMap(tok.nextToken());
1122             } else {
1123                 params = Collections.emptyMap();
1124             }
1125             Source resolved = MCRURIResolver.instance().resolve(target, base);
1126 
1127             try {
1128                 if (resolved != null) {
1129                     if (resolved.getSystemId() == null) {
1130                         resolved.setSystemId(target);
1131                     }
1132                     MCRSourceContent content = new MCRSourceContent(resolved);
1133                     MCRXSLTransformer transformer = getTransformer(stylesheets.split(","));
1134                     MCRParameterCollector paramcollector = MCRParameterCollector.getInstanceFromUserSession();
1135                     paramcollector.setParameters(params);
1136                     MCRContent result = transformer.transform(content, paramcollector);
1137                     return result.getSource();
1138                 } else {
1139                     LOGGER.debug("MCRXslStyleResolver returning empty xml");
1140                     return new JDOMSource(new Element("null"));
1141                 }
1142             } catch (IOException e) {
1143                 Throwable cause = e.getCause();
1144                 while (cause != null) {
1145                     if (cause instanceof TransformerException) {
1146                         throw (TransformerException) cause;
1147                     }
1148                     cause = cause.getCause();
1149                 }
1150                 throw new TransformerException(e);
1151             }
1152         }
1153 
1154         private MCRXSLTransformer getTransformer(String... stylesheet) {
1155             String[] stylesheets = new String[stylesheet.length];
1156             for (int i = 0; i < stylesheets.length; i++) {
1157                 stylesheets[i] = "xsl/" + stylesheet[i] + ".xsl";
1158             }
1159             return MCRXSLTransformer.getInstance(stylesheets);
1160         }
1161     }
1162 
1163     /**
1164      * Transform result of other resolver with stylesheet. Usage: xslTransform:<transformer><?param1=value1
1165      * <&param2=value2>>:<anyMyCoReURI>
1166      */
1167     private static class MCRLayoutTransformerResolver implements URIResolver {
1168 
1169         private static final String TRANSFORMER_FACTORY_PROPERTY = "MCR.Layout.Transformer.Factory";
1170 
1171         @Override
1172         public Source resolve(String href, String base) throws TransformerException {
1173             String help = href.substring(href.indexOf(":") + 1);
1174             String transformerId = new StringTokenizer(help, ":").nextToken();
1175             String target = help.substring(help.indexOf(":") + 1);
1176 
1177             String subUri = target.substring(target.indexOf(":") + 1);
1178             if (subUri.length() == 0) {
1179                 return new JDOMSource(new Element("null"));
1180             }
1181 
1182             Map<String, String> params;
1183             StringTokenizer tok = new StringTokenizer(transformerId, "?");
1184             transformerId = tok.nextToken();
1185 
1186             if (tok.hasMoreTokens()) {
1187                 params = getParameterMap(tok.nextToken());
1188             } else {
1189                 params = Collections.emptyMap();
1190             }
1191             Source resolved = MCRURIResolver.instance().resolve(target, base);
1192 
1193             try {
1194                 if (resolved != null) {
1195                     MCRSourceContent content = new MCRSourceContent(resolved);
1196                     MCRLayoutTransformerFactory factory = MCRConfiguration2
1197                         .<MCRLayoutTransformerFactory>getInstanceOf(TRANSFORMER_FACTORY_PROPERTY)
1198                         .orElseGet(MCRLayoutTransformerFactory::new);
1199                     MCRContentTransformer transformer = factory.getTransformer(transformerId);
1200                     MCRContent result;
1201                     if (transformer instanceof MCRParameterizedTransformer) {
1202                         MCRParameterCollector paramcollector = MCRParameterCollector.getInstanceFromUserSession();
1203                         paramcollector.setParameters(params);
1204                         result = ((MCRParameterizedTransformer) transformer).transform(content, paramcollector);
1205                     } else {
1206                         result = transformer.transform(content);
1207                     }
1208                     return result.getSource();
1209                 } else {
1210                     LOGGER.debug("MCRLayoutStyleResolver returning empty xml");
1211                     return new JDOMSource(new Element("null"));
1212                 }
1213             } catch (Exception e) {
1214                 if (e instanceof TransformerException) {
1215                     throw (TransformerException) e;
1216                 }
1217                 Throwable cause = e.getCause();
1218                 while (cause != null) {
1219                     if (cause instanceof TransformerException) {
1220                         throw (TransformerException) cause;
1221                     }
1222                     cause = cause.getCause();
1223                 }
1224                 throw new TransformerException(e);
1225             }
1226         }
1227 
1228     }
1229 
1230     /**
1231      * <p>
1232      * Includes xsl files which are set in the mycore.properties file.
1233      * </p>
1234      * Example: MCR.URIResolver.xslIncludes.components=iview.xsl,wcms.xsl
1235      * <p>
1236      * Or retrieve the include hrefs from a class implementing
1237      * {@link org.mycore.common.xml.MCRURIResolver.MCRXslIncludeHrefs}. The class. part have to be set, everything after
1238      * class. can be freely choosen.
1239      * </p>
1240      * Example: MCR.URIResolver.xslIncludes.class.template=org.foo.XSLHrefs
1241      *
1242      * @returns A xsl file with the includes as href.
1243      */
1244     private static class MCRXslIncludeResolver implements URIResolver {
1245         private static Logger LOGGER = LogManager.getLogger(MCRXslIncludeResolver.class);
1246 
1247         @Override
1248         public Source resolve(String href, String base) throws TransformerException {
1249             String includePart = href.substring(href.indexOf(":") + 1);
1250             Namespace xslNamespace = Namespace.getNamespace("xsl", "http://www.w3.org/1999/XSL/Transform");
1251 
1252             Element root = new Element("stylesheet", xslNamespace);
1253             root.setAttribute("version", "1.0");
1254 
1255             // get the parameters from mycore.properties
1256             String propertyName = "MCR.URIResolver.xslIncludes." + includePart;
1257             List<String> propValue;
1258             if (includePart.startsWith("class.")) {
1259                 MCRXslIncludeHrefs incHrefClass = MCRConfiguration2
1260                     .getOrThrow(propertyName, MCRConfiguration2::instantiateClass);
1261                 propValue = incHrefClass.getHrefs();
1262             } else {
1263                 propValue = MCRConfiguration2.getString(propertyName)
1264                     .map(MCRConfiguration2::splitValue)
1265                     .map(s -> s.collect(Collectors.toList()))
1266                     .orElseGet(Collections::emptyList);
1267             }
1268 
1269             for (String include : propValue) {
1270                 // create a new include element
1271                 Element includeElement = new Element("include", xslNamespace);
1272                 includeElement.setAttribute("href", include);
1273                 root.addContent(includeElement);
1274                 LOGGER.debug("Resolved XSL include: {}", include);
1275             }
1276             return new JDOMSource(root);
1277         }
1278     }
1279 
1280     /**
1281      * Imports xsl files which are set in the mycore.properties file. Example:
1282      * MCR.URIResolver.xslImports.components=first.xsl,second.xsl Every file must import this URIResolver to form a
1283      * import chain:
1284      *
1285      * <pre>
1286      *  &lt;xsl:import href="xslImport:components:first.xsl"&gt;
1287      * </pre>
1288      *
1289      * @returns A xsl file with the import as href.
1290      */
1291     private static class MCRXslImportResolver implements URIResolver {
1292 
1293         URIResolver fallback = new MCRResourceResolver();
1294 
1295         @Override
1296         public Source resolve(String href, String base) throws TransformerException {
1297             String importXSL = MCRXMLFunctions.nextImportStep(href.substring(href.indexOf(':') + 1));
1298             if (importXSL.isEmpty()) {
1299                 LOGGER.debug("End of import queue: {}", href);
1300                 Namespace xslNamespace = Namespace.getNamespace("xsl", "http://www.w3.org/1999/XSL/Transform");
1301                 Element root = new Element("stylesheet", xslNamespace);
1302                 root.setAttribute("version", "1.0");
1303                 return new JDOMSource(root);
1304             }
1305             LOGGER.debug("xslImport importing {}", importXSL);
1306             return fallback.resolve("resource:xsl/" + importXSL, base);
1307         }
1308     }
1309 
1310     /**
1311      * Builds XML trees from a string representation. Multiple XPath expressions can be separated by &amp; Example:
1312      * buildxml:_rootName_=mycoreobject&metadata/parents/parent/@href= 'FooBar_Document_4711' This will return:
1313      * &lt;mycoreobject&gt; &lt;metadata&gt; &lt;parents&gt; &lt;parent href="FooBar_Document_4711" /&gt;
1314      * &lt;/parents&gt; &lt;/metadata&gt; &lt;/mycoreobject&gt;
1315      */
1316     private static class MCRBuildXMLResolver implements URIResolver {
1317 
1318         private static Hashtable<String, String> getParameterMap(String key) {
1319             String[] param;
1320             StringTokenizer tok = new StringTokenizer(key, "&");
1321             Hashtable<String, String> params = new Hashtable<>();
1322 
1323             while (tok.hasMoreTokens()) {
1324                 param = tok.nextToken().split("=");
1325                 params.put(URLDecoder.decode(param[0], StandardCharsets.UTF_8),
1326                     URLDecoder.decode(param[1], StandardCharsets.UTF_8));
1327             }
1328             return params;
1329         }
1330 
1331         private static void constructElement(Element current, String xpath, String value) {
1332             StringTokenizer st = new StringTokenizer(xpath, "/");
1333             String name = null;
1334             while (st.hasMoreTokens()) {
1335                 name = st.nextToken();
1336                 if (name.startsWith("@")) {
1337                     break;
1338                 }
1339 
1340                 String localName = getLocalName(name);
1341                 Namespace namespace = getNamespace(name);
1342 
1343                 Element child = current.getChild(localName, namespace);
1344                 if (child == null) {
1345                     child = new Element(localName, namespace);
1346                     current.addContent(child);
1347                 }
1348                 current = child;
1349             }
1350 
1351             if (name.startsWith("@")) {
1352                 name = name.substring(1);
1353                 String localName = getLocalName(name);
1354                 Namespace namespace = getNamespace(name);
1355                 current.setAttribute(localName, value, namespace);
1356             } else {
1357                 current.setText(value);
1358             }
1359         }
1360 
1361         private static Namespace getNamespace(String name) {
1362             if (!name.contains(":")) {
1363                 return Namespace.NO_NAMESPACE;
1364             }
1365             String prefix = name.split(":")[0];
1366             Namespace ns = MCRConstants.getStandardNamespace(prefix);
1367             return ns == null ? Namespace.NO_NAMESPACE : ns;
1368         }
1369 
1370         private static String getLocalName(String name) {
1371             if (!name.contains(":")) {
1372                 return name;
1373             } else {
1374                 return name.split(":")[1];
1375             }
1376         }
1377 
1378         /**
1379          * Builds a simple xml node tree on basis of name value pair
1380          */
1381         @Override
1382         public Source resolve(String href, String base) throws TransformerException {
1383             String key = href.substring(href.indexOf(":") + 1);
1384             LOGGER.debug("Building xml from {}", key);
1385 
1386             Hashtable<String, String> params = getParameterMap(key);
1387 
1388             Element defaultRoot = new Element("root");
1389             Element root = defaultRoot;
1390             String rootName = params.get("_rootName_");
1391             if (rootName != null) {
1392                 root = new Element(getLocalName(rootName), getNamespace(rootName));
1393                 params.remove("_rootName_");
1394             }
1395 
1396             for (Map.Entry<String, String> entry : params.entrySet()) {
1397                 constructElement(root, entry.getKey(), entry.getValue());
1398             }
1399             if (root == defaultRoot && root.getChildren().size() > 1) {
1400                 LOGGER.warn("More than 1 root node defined, returning first");
1401                 return new JDOMSource(root.getChildren().get(0).detach());
1402             }
1403             return new JDOMSource(root);
1404         }
1405 
1406     }
1407 
1408     private static class MCRVersionInfoResolver implements URIResolver {
1409 
1410         @Override
1411         public Source resolve(String href, String base) throws TransformerException {
1412             String id = href.substring(href.indexOf(":") + 1);
1413             LOGGER.debug("Reading version info of MCRObject with ID {}", id);
1414             MCRObjectID mcrId = MCRObjectID.getInstance(id);
1415             MCRXMLMetadataManager metadataManager = MCRXMLMetadataManager.instance();
1416             try {
1417                 List<? extends MCRAbstractMetadataVersion<?>> versions = metadataManager.listRevisions(mcrId);
1418                 if (versions != null && !versions.isEmpty()) {
1419                     return getSource(versions);
1420                 } else {
1421                     return getSource(Instant.ofEpochMilli(metadataManager.getLastModified(mcrId))
1422                         .truncatedTo(ChronoUnit.MILLIS));
1423                 }
1424             } catch (Exception e) {
1425                 throw new TransformerException(e);
1426             }
1427         }
1428 
1429         private Source getSource(Instant lastModified) throws IOException {
1430             Element e = new Element("versions");
1431             Element v = new Element("version");
1432             e.addContent(v);
1433             v.setAttribute("date", lastModified.toString());
1434             return new JDOMSource(e);
1435         }
1436 
1437         private Source getSource(List<? extends MCRAbstractMetadataVersion<?>> versions) {
1438             Element e = new Element("versions");
1439             for (MCRAbstractMetadataVersion<?> version : versions) {
1440                 Element v = new Element("version");
1441                 v.setAttribute("user", version.getUser());
1442                 v.setAttribute("date", MCRXMLFunctions.getISODate(version.getDate(), null));
1443                 v.setAttribute("r", version.getRevision());
1444                 v.setAttribute("action", Character.toString(version.getType()));
1445                 e.addContent(v);
1446             }
1447             return new JDOMSource(e);
1448         }
1449     }
1450 
1451     private static class MCRDeletedObjectResolver implements URIResolver {
1452 
1453         /**
1454          * Returns a deleted mcr object xml for the given id. If there is no such object a dummy object with an empty
1455          * metadata element is returned.
1456          *
1457          * @param href
1458          *            an uri starting with <code>deletedMcrObject:</code>
1459          * @param base
1460          *            may be null
1461          */
1462         @Override
1463         public Source resolve(String href, String base) throws TransformerException {
1464             String[] parts = href.split(":");
1465             MCRObjectID mcrId = MCRObjectID.getInstance(parts[parts.length - 1]);
1466             LOGGER.info("Resolving deleted object {}", mcrId);
1467             try {
1468                 MCRContent lastPresentVersion = MCRXMLMetadataManager.instance().retrieveContent(mcrId);
1469                 if (lastPresentVersion == null) {
1470                     LOGGER.warn("Could not resolve deleted object {}", mcrId);
1471                     return new JDOMSource(MCRObjectFactory.getSampleObject(mcrId));
1472                 }
1473                 return lastPresentVersion.getSource();
1474             } catch (IOException e) {
1475                 throw new TransformerException(e);
1476             }
1477         }
1478     }
1479 
1480     private static class MCRFileMetadataResolver implements URIResolver {
1481 
1482         @Override
1483         public Source resolve(String href, String base) throws TransformerException {
1484             String[] parts = href.split(":");
1485             String completePath = parts[1];
1486             String[] pathParts = completePath.split("/", 2);
1487             MCRObjectID derivateID = MCRObjectID.getInstance(pathParts[0]);
1488             MCRDerivate derivate = MCRMetadataManager.retrieveMCRDerivate(derivateID);
1489             MCRObjectDerivate objectDerivate = derivate.getDerivate();
1490             if (pathParts.length == 1) {
1491                 //only derivate is given;
1492                 Element fileset = new Element("fileset");
1493                 if (objectDerivate.getURN() != null) {
1494                     fileset.setAttribute("urn", objectDerivate.getURN());
1495                     for (MCRFileMetadata fileMeta : objectDerivate.getFileMetadata()) {
1496                         fileset.addContent(fileMeta.createXML());
1497                     }
1498                 }
1499                 return new JDOMSource(fileset);
1500             }
1501             MCRFileMetadata fileMetadata = objectDerivate.getOrCreateFileMetadata("/" + pathParts[1]);
1502             return new JDOMSource(fileMetadata.createXML());
1503         }
1504     }
1505 
1506     /**
1507      * Redirect to different URIResolver that is defined via property. This resolver is meant to serve static content as
1508      * no variable substitution takes place Example: MCR.URIResolver.redirect.alias=webapp:path/to/alias.xml
1509      */
1510     private static class MCRRedirectResolver implements URIResolver {
1511         private static final Logger LOGGER = LogManager.getLogger(MCRRedirectResolver.class);
1512 
1513         @Override
1514         public Source resolve(String href, String base) throws TransformerException {
1515             String configsuffix = href.substring(href.indexOf(":") + 1);
1516 
1517             // get the parameters from mycore.properties
1518             String propertyName = "MCR.URIResolver.redirect." + configsuffix;
1519             String propValue = MCRConfiguration2.getStringOrThrow(propertyName);
1520             LOGGER.info("Redirect {} to {}", href, propValue);
1521             return singleton.resolve(propValue, base);
1522         }
1523     }
1524 
1525     /**
1526      * Resolves an data url and returns the content.
1527      *
1528      * @see MCRDataURL
1529      */
1530     private static class MCRDataURLResolver implements URIResolver {
1531 
1532         @Override
1533         public Source resolve(String href, String base) throws TransformerException {
1534             try {
1535                 final MCRDataURL dataURL = MCRDataURL.parse(href);
1536 
1537                 final MCRByteContent content = new MCRByteContent(dataURL.getData());
1538                 content.setSystemId(href);
1539                 content.setMimeType(dataURL.getMimeType());
1540                 content.setEncoding(dataURL.getCharset().name());
1541 
1542                 return content.getSource();
1543             } catch (IOException e) {
1544                 throw new TransformerException(e);
1545             }
1546         }
1547 
1548     }
1549 
1550     private static class MCRI18NResolver implements URIResolver {
1551 
1552         /**
1553          * Resolves the I18N String value for the given property.<br><br>
1554          * <br>
1555          * Syntax: <code>i18n:{i18n-code},{i18n-prefix}*,{i18n-prefix}*...</code> or <br>
1556          *         <code>i18n:{i18n-code}[:param1:param2:…paramN]</code>
1557          * <br>
1558          * Result: <code> <br>
1559          *     &lt;i18n&gt; <br>
1560          *   &lt;translation key=&quot;key1&quot;&gt;translation1&lt;/translation&gt; <br>
1561          *   &lt;translation key=&quot;key2&quot;&gt;translation2&lt;/translation&gt; <br>
1562          *   &lt;translation key=&quot;key3&quot;&gt;translation3&lt;/translation&gt; <br>
1563          * &lt;/i18n&gt; <br>
1564          * </code>
1565          * <br/>
1566          * If just one i18n-code is passed, then the translation element is skipped.
1567          * <code>
1568          *     &lt;i18n&gt; <br>translation&lt;/i18n&gt;<br>
1569          * </code>
1570          * Additionally, if the singular i18n-code is followed by a ":"-separated list of values,
1571          * the translation result is interpreted to be in Java MessageFormat and will be formatted with those values.
1572          * E.g.
1573          * <code>i18n:module.dptbase.common.results.nResults:15</code> (<code>{0} objects found</code>)
1574          *  -> "<code>15 objects found</code>"
1575          * @param href
1576          *            URI in the syntax above
1577          * @param base
1578          *            not used
1579          *
1580          * @return the element with result format above
1581          * @see javax.xml.transform.URIResolver
1582          */
1583         @Override
1584         public Source resolve(String href, String base) {
1585             String target = href.substring(href.indexOf(":") + 1);
1586 
1587             final Element i18nElement = new Element("i18n");
1588             if (!target.contains("*") && !target.contains(",")) {
1589                 String translation;
1590                 if (target.contains(":")) {
1591                     final int i = target.indexOf(":");
1592                     translation = MCRTranslation.translate(target.substring(0, i),
1593                         (Object[]) target.substring(i + 1).split(":"));
1594                 } else {
1595                     translation = MCRTranslation.translate(target);
1596                 }
1597                 i18nElement.addContent(translation);
1598                 return new JDOMSource(i18nElement);
1599             }
1600 
1601             final String[] translationKeys = target.split(",");
1602 
1603             // Combine translations to prevent duplicates
1604             HashMap<String, String> translations = new HashMap<>();
1605             for (String translationKey : translationKeys) {
1606                 if (translationKey.endsWith("*")) {
1607                     final String prefix = translationKey.substring(0, translationKey.length() - 1);
1608                     translations.putAll(MCRTranslation.translatePrefix(prefix));
1609                 } else {
1610                     translations.put(translationKey,
1611                         MCRTranslation.translate(translationKey));
1612                 }
1613             }
1614 
1615             translations.forEach((key, value) -> {
1616                 final Element translation = new Element("translation");
1617                 translation.setAttribute("key", key);
1618                 translation.setText(value);
1619                 i18nElement.addContent(translation);
1620             });
1621 
1622             return new JDOMSource(i18nElement);
1623         }
1624     }
1625 
1626     private static class MCRCheckPermissionChainResolver implements URIResolver {
1627         /**
1628          * Checks the permission and if granted resolve the uri
1629          *
1630          * Syntax: <code>checkPermissionChain:{?id}:{permission}:{$uri}</code>
1631          *
1632          * @param href
1633          *            URI in the syntax above
1634          * @param base
1635          *            not used
1636          *
1637          * @return if you have the permission then the resolved uri otherwise an Exception
1638          * @see javax.xml.transform.URIResolver
1639          */
1640         @Override
1641         public Source resolve(String href, String base) throws TransformerException {
1642             final String[] split = href.split(":", 4);
1643 
1644             if (split.length != 4) {
1645                 throw new MCRException(
1646                     "Syntax needs to be checkPermissionChain:{?id}:{permission}:{uri} but was " + href);
1647             }
1648 
1649             final String permission = split[2];
1650 
1651             final String uri = split[3];
1652             final boolean hasAccess;
1653 
1654             if (!split[1].isBlank()) {
1655                 hasAccess = MCRAccessManager.checkPermission(split[1], permission);
1656             } else {
1657                 hasAccess = MCRAccessManager.checkPermission(permission);
1658             }
1659 
1660             if (!hasAccess) {
1661                 throw new TransformerException("No Access to " + uri + " (" + href + " )");
1662             }
1663 
1664             return MCRURIResolver.instance().resolve(uri, base);
1665         }
1666     }
1667 
1668     private static class MCRCheckPermissionResolver implements URIResolver {
1669         /**
1670          * returns the boolean value for the given ACL permission.
1671          *
1672          * Syntax: <code>checkPermission:{id}:{permission}</code> or <code>checkPermission:{permission}</code>
1673          * 
1674          * @param href
1675          *            URI in the syntax above
1676          * @param base
1677          *            not used
1678          * 
1679          * @return the root element "boolean" of the XML document with content string true of false
1680          * @see javax.xml.transform.URIResolver
1681          */
1682         @Override
1683         public Source resolve(String href, String base) throws TransformerException {
1684             final String[] split = href.split(":");
1685             boolean permission;
1686             switch (split.length) {
1687             case 2:
1688                 permission = MCRAccessManager.checkPermission(split[1]);
1689                 break;
1690             case 3:
1691                 permission = MCRAccessManager.checkPermission(split[1], split[2]);
1692                 break;
1693             default:
1694                 throw new IllegalArgumentException(
1695                     "Invalid format of uri for retrieval of checkPermission: " + href);
1696             }
1697             Element root = new Element("boolean");
1698             root.setText(Boolean.toString(permission));
1699             return new JDOMSource(root);
1700         }
1701     }
1702 
1703     /**
1704      * @author Frank L\u00FCtzenkirchen
1705      */
1706     private static class MCRCachingResolver implements URIResolver {
1707 
1708         private final static Logger LOGGER = LogManager.getLogger();
1709 
1710         private final static String CONFIG_PREFIX = "MCR.URIResolver.CachingResolver";
1711 
1712         private long maxAge;
1713 
1714         private MCRCache<String, Element> cache;
1715 
1716         MCRCachingResolver() {
1717             int capacity = MCRConfiguration2.getOrThrow(CONFIG_PREFIX + ".Capacity", Integer::parseInt);
1718             maxAge = MCRConfiguration2.getOrThrow(CONFIG_PREFIX + ".MaxAge", Long::parseLong);
1719             cache = new MCRCache<>(capacity, MCRCachingResolver.class.getName());
1720         }
1721 
1722         /**
1723          * Resolves XML content from a given URI and caches it for re-use.
1724          *
1725          * If the URI was already resolved within the last
1726          * MCR.URIResolver.CachingResolver.MaxAge milliseconds, the cached version is returned.
1727          * The default max age is one hour.
1728          *
1729          * The cache capacity is configured via MCR.URIResolver.CachingResolver.Capacity
1730          * The default capacity is 100.
1731          */
1732         @Override
1733         public Source resolve(String href, String base) throws TransformerException {
1734             String hrefToCache = href.substring(href.indexOf(":") + 1);
1735             LOGGER.debug("resolving: " + hrefToCache);
1736 
1737             long maxDateCached = System.currentTimeMillis() - maxAge;
1738             Element resolvedXML = cache.getIfUpToDate(hrefToCache, maxDateCached);
1739 
1740             if (resolvedXML == null) {
1741                 LOGGER.debug(hrefToCache + " not in cache, must resolve");
1742                 resolvedXML = MCRURIResolver.instance().resolve(hrefToCache);
1743                 cache.put(hrefToCache, resolvedXML);
1744             } else {
1745                 LOGGER.debug(hrefToCache + " already in cache");
1746             }
1747 
1748             return new JDOMSource(resolvedXML);
1749         }
1750     }
1751 
1752 }