001/*
002 *  Copyright 2013 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.runtime.cocoon;
017
018import java.io.File;
019import java.io.IOException;
020import java.io.InputStream;
021import java.lang.reflect.Method;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.HashMap;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Map;
028
029import javax.xml.transform.Result;
030import javax.xml.transform.Templates;
031import javax.xml.transform.Transformer;
032import javax.xml.transform.TransformerConfigurationException;
033import javax.xml.transform.TransformerException;
034import javax.xml.transform.TransformerFactory;
035import javax.xml.transform.URIResolver;
036import javax.xml.transform.sax.SAXTransformerFactory;
037import javax.xml.transform.sax.TemplatesHandler;
038import javax.xml.transform.sax.TransformerHandler;
039import javax.xml.transform.stream.StreamSource;
040
041import org.apache.avalon.framework.activity.Disposable;
042import org.apache.avalon.framework.activity.Initializable;
043import org.apache.avalon.framework.context.Context;
044import org.apache.avalon.framework.context.ContextException;
045import org.apache.avalon.framework.context.Contextualizable;
046import org.apache.avalon.framework.logger.AbstractLogEnabled;
047import org.apache.avalon.framework.parameters.ParameterException;
048import org.apache.avalon.framework.parameters.Parameterizable;
049import org.apache.avalon.framework.parameters.Parameters;
050import org.apache.avalon.framework.service.ServiceException;
051import org.apache.avalon.framework.service.ServiceManager;
052import org.apache.avalon.framework.service.Serviceable;
053import org.apache.cocoon.components.ContextHelper;
054import org.apache.cocoon.components.xslt.TraxErrorListener;
055import org.apache.cocoon.environment.Request;
056import org.apache.commons.lang.BooleanUtils;
057import org.apache.excalibur.source.Source;
058import org.apache.excalibur.source.SourceException;
059import org.apache.excalibur.source.SourceResolver;
060import org.apache.excalibur.source.SourceValidity;
061import org.apache.excalibur.xml.sax.XMLizable;
062import org.apache.excalibur.xml.xslt.XSLTProcessor;
063import org.apache.excalibur.xml.xslt.XSLTProcessorException;
064import org.apache.excalibur.xmlizer.XMLizer;
065import org.xml.sax.ContentHandler;
066import org.xml.sax.InputSource;
067import org.xml.sax.SAXException;
068import org.xml.sax.XMLFilter;
069
070import org.ametys.runtime.config.Config;
071
072/**
073 * Adaptation of Excalibur's XSLTProcessor implementation to allow for better error reporting. This implementation is also threadsafe.<br>
074 * It also handles a {@link Templates} cache, for performance purpose.
075 */
076public class ThreadSafeTraxProcessor extends AbstractLogEnabled implements XSLTProcessor, Serviceable, Initializable, Disposable, Parameterizable, URIResolver, Contextualizable
077{
078    private static final String __URI_CACHE_ATTR = "cache.xslt.resolvedURIs";
079    
080    /** The configured transformer factory to use */
081    private String _transformerFactory;
082
083    /** The trax TransformerFactory this component uses */
084    private SAXTransformerFactory _factory;
085
086    /** Is incremental processing turned on? (default for Xalan: no) */
087    private boolean _incrementalProcessing;
088
089    /** Resolver used to resolve XSLT document() calls, imports and includes */
090    private SourceResolver _resolver;
091
092    private XMLizer _xmlizer;
093
094    /** The ServiceManager */
095    private ServiceManager _manager;
096    
097    private Context _context;
098    
099    private boolean _dontUseCache;
100    
101    // the XSLT cache
102    private Map<String, TemplatesCache> _templatesCache = new HashMap<>();
103    
104    @Override
105    public void contextualize(Context context) throws ContextException
106    {
107        _context = context;
108    }
109
110    /**
111     * Compose. Try to get the store
112     */
113    public void service(final ServiceManager manager) throws ServiceException
114    {
115        _manager = manager;
116        _xmlizer = (XMLizer) _manager.lookup(XMLizer.ROLE);
117        _resolver = (SourceResolver) _manager.lookup(SourceResolver.ROLE);
118    }
119
120    /**
121     * Initialize
122     */
123    public void initialize() throws Exception
124    {
125        _factory = _createTransformerFactory(_transformerFactory);
126        
127        Config config = Config.getInstance();
128        if (config != null)
129        {
130            // not in safe mode
131            _dontUseCache = false;
132        }
133    }
134
135    /**
136     * Disposable
137     */
138    public void dispose()
139    {
140        if (null != _manager)
141        {
142            _manager.release(_resolver);
143            _manager.release(_xmlizer);
144            _manager = null;
145        }
146        _xmlizer = null;
147        _resolver = null;
148        _templatesCache.clear();
149    }
150
151    /**
152     * Configure the component
153     */
154    public void parameterize(final Parameters params) throws ParameterException
155    {
156        _incrementalProcessing = params.getParameterAsBoolean("incremental-processing", this._incrementalProcessing);
157        _transformerFactory = params.getParameter("transformer-factory", null);
158    }
159
160    public void setTransformerFactory(final String classname)
161    {
162        throw new UnsupportedOperationException("This implementation is threadsafe, so the TransformerFactory cannot be changed");
163    }
164
165    public TransformerHandler getTransformerHandler(final Source stylesheet) throws XSLTProcessorException
166    {
167        return getTransformerHandler(stylesheet, null);
168    }
169
170    public TransformerHandler getTransformerHandler(final Source stylesheet, final XMLFilter filter) throws XSLTProcessorException
171    {
172        final XSLTProcessor.TransformerHandlerAndValidity validity = getTransformerHandlerAndValidity(stylesheet, filter);
173        return validity.getTransfomerHandler();
174    }
175
176    public TransformerHandlerAndValidity getTransformerHandlerAndValidity(final Source stylesheet) throws XSLTProcessorException
177    {
178        return getTransformerHandlerAndValidity(stylesheet, null);
179    }
180
181    public TransformerHandlerAndValidity getTransformerHandlerAndValidity(Source stylesheet, XMLFilter filter) throws XSLTProcessorException
182    {
183        TraxErrorListener errorListener = new TraxErrorListener(getLogger(), stylesheet.getURI());
184        try
185        {
186            // get Templates from cache or create it
187            Templates template = _getTemplates(stylesheet, filter);
188            
189            // Create transformer handler
190            TransformerHandler handler = _factory.newTransformerHandler(template);
191            handler.getTransformer().setErrorListener(errorListener);
192            handler.getTransformer().setURIResolver(this);
193
194            // Create result
195            SourceValidity validity = stylesheet.getValidity();
196            TransformerHandlerAndValidity handlerAndValidity = new MyTransformerHandlerAndValidity(handler, validity);
197
198            return handlerAndValidity;
199        }
200        catch (IOException e)
201        {
202            throw new XSLTProcessorException("Exception when getting Templates for " + stylesheet.getURI(), e);
203        }
204        catch (TransformerConfigurationException e)
205        {
206            Throwable realEx = errorListener.getThrowable();
207            if (realEx == null)
208            {
209                realEx = e;
210            }
211
212            if (realEx instanceof RuntimeException)
213            {
214                throw (RuntimeException) realEx;
215            }
216
217            if (realEx instanceof XSLTProcessorException)
218            {
219                throw (XSLTProcessorException) realEx;
220            }
221
222            throw new XSLTProcessorException("Exception when creating Transformer from " + stylesheet.getURI(), realEx);
223        }
224    }
225    
226    private Templates _getTemplates(Source stylesheet, XMLFilter filter) throws XSLTProcessorException, IOException
227    {
228        String uri = stylesheet.getURI().intern();
229        
230        // synchronize on XSL name to avoid concurrent write access to the cache for a given stylesheet
231        synchronized (uri)
232        {
233            TemplatesCache cachedTemplates = _templatesCache.get(uri);
234            
235            if (!_dontUseCache)
236            {
237                if (cachedTemplates != null && cachedTemplates.getLastModified() == stylesheet.getLastModified())
238                {
239                    CachedTemplates templates = _getCachedTemplates(cachedTemplates.getCollection());
240                    
241                    if (templates != null)
242                    {
243                        if (getLogger().isDebugEnabled())
244                        {
245                            getLogger().debug("Found Templates in cache for stylesheet : " + uri);
246                        }
247                        
248                        return templates.getTemplates();
249                    }
250                }
251                else
252                {
253                    cachedTemplates = new TemplatesCache(stylesheet.getLastModified());
254                    _templatesCache.put(uri, cachedTemplates);
255                }
256            }
257            
258            CachedTemplates templates = _createTemplates(stylesheet, filter);
259            
260            if (getLogger().isDebugEnabled())
261            {
262                String[] rawURIs = templates.getRawURIs();
263                String[] resolvedURIs = templates.getResolvedURIs();
264                
265                StringBuilder sb = new StringBuilder("Templates created for stylesheet : ");
266                sb.append(uri);
267                sb.append(" including the following stylesheets : ");
268                for (int i = 0; i < rawURIs.length; i++)
269                {
270                    sb.append('\n');
271                    sb.append(rawURIs[i]);
272                    sb.append(" => ");
273                    sb.append(resolvedURIs[i]);
274                }
275                
276                getLogger().debug(sb.toString());
277            }
278            
279            if (!_dontUseCache)
280            {
281                cachedTemplates.getCollection().add(templates);
282            }
283            
284            return templates.getTemplates();
285        }
286    }
287    
288    @SuppressWarnings("unchecked")
289    private CachedTemplates _getCachedTemplates(Collection<CachedTemplates> cachedTemplates) throws IOException
290    {
291        CachedTemplates outOfDateTemplates = null;
292        
293        Iterator<CachedTemplates> it = cachedTemplates.iterator();
294        
295        Request request = null;
296        try
297        {
298            request = ContextHelper.getRequest(_context);
299        }
300        catch (Exception e)
301        {
302            // ignore, there's simply no current request
303        }
304        
305        // very simple cache for storing raw/resolved URI pairs to avoid unnecessary calls to SourceResolver 
306        Map<UnresolvedURI, ResolvedURI> resolutionCache = null;
307        if (request != null)
308        {
309            resolutionCache = (Map<UnresolvedURI, ResolvedURI>) request.getAttribute(__URI_CACHE_ATTR);
310            if (resolutionCache == null)
311            {
312                resolutionCache = new HashMap<>();
313                request.setAttribute(__URI_CACHE_ATTR, resolutionCache);
314            }
315        }
316        else
317        {
318            resolutionCache = new HashMap<>();
319        }
320        
321        
322        while (outOfDateTemplates == null && it.hasNext())
323        {
324            CachedTemplates templates = it.next();
325            
326            int validity = _isValid(templates, resolutionCache);
327            if (validity == 1)
328            {
329                return templates;
330            }
331            
332            if (validity == 0)
333            {
334                outOfDateTemplates = templates;
335            }
336        }
337        
338        if (outOfDateTemplates != null)
339        {
340            cachedTemplates.remove(outOfDateTemplates);
341        }
342        
343        return null;
344    }
345    
346    private int _isValid(CachedTemplates templates, Map<UnresolvedURI, ResolvedURI> resolutionCache) throws IOException
347    {
348        // the current Templates object is valid if and only if the resolution of raw URIs correspond to stored resolved URIs
349        String[] rawURIs = templates.getRawURIs();
350        String[] baseURIs = templates.getBaseURIs();
351        String[] resolvedURIs = templates.getResolvedURIs();
352        Long[] timestamps = templates.getTimestamps();
353        
354        boolean isOutOfDate = false;
355        
356        for (int i = 0; i < rawURIs.length; i++)
357        {
358            // small optimization in the case where the same resolution has already been requested in the current context
359            UnresolvedURI unresolved = new UnresolvedURI(rawURIs[i], baseURIs[i]);
360            ResolvedURI resolved = resolutionCache.get(new UnresolvedURI(rawURIs[i], baseURIs[i]));
361            
362            String resolvedURI;
363            long lastModified;
364            if (resolved != null)
365            {
366                resolvedURI = resolved._resolvedURI;
367                lastModified = resolved._timestamp;
368            }
369            else
370            {
371                Source src = _resolve(rawURIs[i], baseURIs[i]);
372                resolvedURI = src.getURI();
373                lastModified = src.getLastModified();
374                resolutionCache.put(unresolved, new ResolvedURI(resolvedURI, lastModified));
375            }
376            
377            if (!resolvedURI.equals(resolvedURIs[i]))
378            {
379                return -1;
380            }
381            
382            if (lastModified == 0 || timestamps[i] == 0 || lastModified != timestamps[i])
383            {
384                isOutOfDate = true;
385            }
386        }
387        
388        return isOutOfDate ? 0 : 1;
389    }
390    
391    @SuppressWarnings("unchecked")
392    private CachedTemplates _createTemplates(Source stylesheet, XMLFilter filter) throws XSLTProcessorException
393    {
394        String id = stylesheet.getURI();
395
396        // Do not reuse the global SAXTransformerFactory, as we set a different URIResolver and ErrorListener
397        TraxErrorListener errorListener = new TraxErrorListener(getLogger(), id);
398        CachedTemplates cachedTemplates = new CachedTemplates();
399        
400        SAXTransformerFactory factory = _createTransformerFactory(_transformerFactory);
401        factory.setErrorListener(errorListener);
402        factory.setURIResolver(cachedTemplates);
403        try
404        {
405            if (getLogger().isDebugEnabled())
406            {
407                getLogger().debug("Creating new Templates for " + id);
408            }
409
410            factory.setErrorListener(errorListener);
411
412            // Create a Templates ContentHandler to handle parsing of the
413            // stylesheet.
414            TemplatesHandler templatesHandler = factory.newTemplatesHandler();
415
416            // Set the system ID for the template handler since some
417            // TrAX implementations (XSLTC) rely on this in order to obtain
418            // a meaningful identifier for the Templates instances.
419            templatesHandler.setSystemId(id);
420            if (filter != null)
421            {
422                filter.setContentHandler(templatesHandler);
423            }
424
425            if (getLogger().isDebugEnabled())
426            {
427                getLogger().debug("Source = " + stylesheet + ", templatesHandler = " + templatesHandler);
428            }
429
430            // Process the stylesheet.
431            _sourceToSAX(stylesheet, filter != null ? (ContentHandler) filter : (ContentHandler) templatesHandler);
432
433            // Get the Templates object (generated during the parsing of
434            // the stylesheet) from the TemplatesHandler.
435            final Templates template = templatesHandler.getTemplates();
436
437            if (null == template)
438            {
439                throw new XSLTProcessorException("Unable to create templates for stylesheet: " + stylesheet.getURI());
440            }
441
442            // Must set base for Xalan stylesheet.
443            // Otherwise document('') in logicsheet causes NPE.
444            Class clazz = template.getClass();
445            if (clazz.getName().equals("org.apache.xalan.templates.StylesheetRoot"))
446            {
447                Method method = clazz.getMethod("setHref", new Class[] {String.class});
448                method.invoke(template, new Object[] {id});
449            }
450            
451            cachedTemplates.setTemplates(template);
452            
453            return cachedTemplates;
454        }
455        catch (Exception e)
456        {
457            Throwable realEx = errorListener.getThrowable();
458            if (realEx == null)
459            {
460                realEx = e;
461            }
462
463            if (realEx instanceof RuntimeException)
464            {
465                throw (RuntimeException) realEx;
466            }
467
468            if (realEx instanceof XSLTProcessorException)
469            {
470                throw (XSLTProcessorException) realEx;
471            }
472
473            throw new XSLTProcessorException("Exception when creating Transformer from " + stylesheet.getURI(), realEx);
474        }
475    }
476    
477    private void _sourceToSAX(Source source, ContentHandler handler) throws SAXException, IOException, SourceException
478    {
479        if (source instanceof XMLizable)
480        {
481            ((XMLizable) source).toSAX(handler);
482        }
483        else
484        {
485            final InputStream inputStream = source.getInputStream();
486            final String mimeType = source.getMimeType();
487            final String systemId = source.getURI();
488            _xmlizer.toSAX(inputStream, mimeType, systemId, handler);
489        }
490    }
491
492    public void transform(final Source source, final Source stylesheet, final Parameters params, final Result result) throws XSLTProcessorException
493    {
494        try
495        {
496            if (getLogger().isDebugEnabled())
497            {
498                getLogger().debug("Transform source = " + source + ", stylesheet = " + stylesheet + ", parameters = " + params + ", result = " + result);
499            }
500            final TransformerHandler handler = getTransformerHandler(stylesheet);
501            if (params != null)
502            {
503                final Transformer transformer = handler.getTransformer();
504                transformer.clearParameters();
505                String[] names = params.getNames();
506                for (int i = names.length - 1; i >= 0; i--)
507                {
508                    transformer.setParameter(names[i], params.getParameter(names[i]));
509                }
510            }
511
512            handler.setResult(result);
513            _sourceToSAX(source, handler);
514            if (getLogger().isDebugEnabled())
515            {
516                getLogger().debug("Transform done");
517            }
518        }
519        catch (SAXException e)
520        {
521            // Unwrapping the exception will "remove" the real cause with
522            // never Xalan versions and makes the exception message unusable
523            final String message = "Error in running Transformation";
524            throw new XSLTProcessorException(message, e);
525            /*
526             * if( e.getException() == null ) { final String message = "Error in running Transformation"; throw new XSLTProcessorException( message, e ); } else { final String message = "Got SAXException. Rethrowing cause exception."; getLogger().debug( message, e ); throw new XSLTProcessorException( "Error in running Transformation", e.getException() ); }
527             */
528        }
529        catch (Exception e)
530        {
531            final String message = "Error in running Transformation";
532            throw new XSLTProcessorException(message, e);
533        }
534    }
535
536    /**
537     * Get the TransformerFactory associated with the given classname. If the class can't be found or the given class doesn't implement the required interface, the default factory is returned.
538     * @param factoryName The name of the factory class to create
539     * @return The instance created
540     */
541    private SAXTransformerFactory _createTransformerFactory(String factoryName)
542    {
543        SAXTransformerFactory saxFactory;
544
545        if (null == factoryName)
546        {
547            saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
548        }
549        else
550        {
551            try
552            {
553                ClassLoader loader = Thread.currentThread().getContextClassLoader();
554                if (loader == null)
555                {
556                    loader = getClass().getClassLoader();
557                }
558                
559                saxFactory = (SAXTransformerFactory) loader.loadClass(factoryName).newInstance();
560            }
561            catch (ClassNotFoundException cnfe)
562            {
563                getLogger().error("Cannot find the requested TrAX factory '" + factoryName + "'. Using default TrAX Transformer Factory instead.");
564                if (_factory != null)
565                {
566                    return _factory;
567                }
568                
569                saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
570            }
571            catch (ClassCastException cce)
572            {
573                getLogger().error("The indicated class '" + factoryName + "' is not a TrAX Transformer Factory. Using default TrAX Transformer Factory instead.");
574                if (_factory != null)
575                {
576                    return _factory;
577                }
578                
579                saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
580            }
581            catch (Exception e)
582            {
583                getLogger().error("Error found loading the requested TrAX Transformer Factory '" + factoryName + "'. Using default TrAX Transformer Factory instead.");
584                if (_factory != null)
585                {
586                    return _factory;
587                }
588                
589                saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
590            }
591        }
592
593        saxFactory.setErrorListener(new TraxErrorListener(getLogger(), null));
594        saxFactory.setURIResolver(this);
595
596        if (saxFactory.getClass().getName().equals("org.apache.xalan.processor.TransformerFactoryImpl"))
597        {
598            saxFactory.setAttribute("http://xml.apache.org/xalan/features/incremental", BooleanUtils.toBooleanObject(_incrementalProcessing));
599        }
600        // SAXON 8 will not report errors unless version warning is set to false.
601        if (saxFactory.getClass().getName().equals("net.sf.saxon.TransformerFactoryImpl"))
602        {
603            saxFactory.setAttribute("http://saxon.sf.net/feature/version-warning", Boolean.FALSE);
604        }
605
606        return saxFactory;
607    }
608
609    /**
610     * Called by the processor when it encounters an xsl:include, xsl:import, or document() function.
611     * 
612     * @param href An href attribute, which may be relative or absolute.
613     * @param base The base URI in effect when the href attribute was encountered.
614     * 
615     * @return A Source object, or null if the href cannot be resolved, and the processor should try to resolve the URI itself.
616     * 
617     * @throws TransformerException if an error occurs when trying to resolve the URI.
618     */
619    public javax.xml.transform.Source resolve(String href, String base) throws TransformerException
620    {
621        return _resolve(href, base, null, null, null, null);
622    }
623    
624    @SuppressWarnings("deprecation") 
625    private Source _resolve(String href, String base) throws IOException
626    {
627        Source xslSource = null;
628
629        if (base == null || href.indexOf(":") > 1)
630        {
631            // Null base - href must be an absolute URL
632            xslSource = _resolver.resolveURI(href);
633        }
634        else if (href.length() == 0)
635        {
636            // Empty href resolves to base
637            xslSource = _resolver.resolveURI(base);
638        }
639        else
640        {
641            // is the base a file or a real m_url
642            if (!base.startsWith("file:"))
643            {
644                int lastPathElementPos = base.lastIndexOf('/');
645                if (lastPathElementPos == -1)
646                {
647                    // this should never occur as the base should
648                    // always be protocol:/....
649                    return null; // we can't resolve this
650                }
651                else
652                {
653                    xslSource = _resolver.resolveURI(base.substring(0, lastPathElementPos) + "/" + href);
654                }
655            }
656            else
657            {
658                File parent = new File(base.substring(5));
659                File parent2 = new File(parent.getParentFile(), href);
660                xslSource = _resolver.resolveURI(parent2.toURL().toExternalForm());
661            }
662        }
663        
664        return xslSource;
665    }
666    
667    javax.xml.transform.Source _resolve(String href, String base, Collection<String> rawURIs, Collection<String> baseURIs, Collection<Long> timestamps, Collection<String> resolvedURIs)
668    {
669        if (getLogger().isDebugEnabled())
670        {
671            getLogger().debug("resolve(href = " + href + ", base = " + base + "); resolver = " + _resolver);
672        }
673
674        Source xslSource = null;
675        try
676        {
677            xslSource = _resolve(href, base);
678            
679            if (rawURIs != null)
680            {
681                rawURIs.add(href);
682            }
683            
684            if (baseURIs != null)
685            {
686                baseURIs.add(base);
687            }
688            
689            if (timestamps != null)
690            {
691                timestamps.add(xslSource.getLastModified());
692            }
693
694            if (resolvedURIs != null)
695            {
696                resolvedURIs.add(xslSource.getURI());
697            }
698
699            InputSource is = _getInputSource(xslSource);
700
701            if (getLogger().isDebugEnabled())
702            {
703                getLogger().debug("xslSource = " + xslSource + ", system id = " + xslSource.getURI());
704            }
705
706            return new StreamSource(is.getByteStream(), is.getSystemId());
707        }
708        catch (IOException ioe)
709        {
710            if (getLogger().isDebugEnabled())
711            {
712                getLogger().debug("Failed to resolve " + href + "(base = " + base + "), return null", ioe);
713            }
714
715            return null;
716        }
717        finally
718        {
719            _resolver.release(xslSource);
720        }
721    }
722
723    /**
724     * Return a new <code>InputSource</code> object that uses the <code>InputStream</code> and the system ID of the <code>Source</code> object.
725     * @param source The source concerned
726     * @return The input source
727     * @throws IOException if I/O error occurred.
728     * @throws SourceException if an error occurred.
729     */
730    private InputSource _getInputSource(final Source source) throws IOException, SourceException
731    {
732        final InputSource newObject = new InputSource(source.getInputStream());
733        newObject.setSystemId(source.getURI());
734        return newObject;
735    }
736
737    /**
738     * Subclass to allow for instanciation, as for some unknown reason the constructor is protected....
739     */
740    static class MyTransformerHandlerAndValidity extends TransformerHandlerAndValidity
741    {
742        MyTransformerHandlerAndValidity(TransformerHandler handler, SourceValidity validity)
743        {
744            super(handler, validity);
745        }
746    }
747    
748    // all known Templates for a single input stylesheet
749    private class CachedTemplates implements URIResolver
750    {
751        // non-resolved included/imported URIs for the input stylesheet
752        private List<String> _rawURIs = new ArrayList<>();
753        
754        // base URIs for resolution
755        private List<String> _baseURIs = new ArrayList<>();
756        
757        // resolved URIs
758        private List<String> _resolvedURIs = new ArrayList<>();
759        
760        // last modified timestamps for resolved URIs
761        private List<Long> _timestamps = new ArrayList<>();
762        
763        // resulting templates
764        private Templates _templates;
765        
766        CachedTemplates()
767        {
768            // empty
769        }
770
771        @Override
772        public javax.xml.transform.Source resolve(String href, String base) throws TransformerException
773        {
774            return _resolve(href, base, _rawURIs, _baseURIs, _timestamps, _resolvedURIs);
775        }
776        
777        String[] getRawURIs()
778        {
779            return _rawURIs.toArray(new String[]{});
780        }
781        
782        String[] getBaseURIs()
783        {
784            return _baseURIs.toArray(new String[]{});
785        }
786        
787        Long[] getTimestamps()
788        {
789            return _timestamps.toArray(new Long[]{});
790        }
791        
792        String[] getResolvedURIs()
793        {
794            return _resolvedURIs.toArray(new String[]{});
795        }
796        
797        Templates getTemplates()
798        {
799            return _templates;
800        }
801        
802        void setTemplates(Templates templates)
803        {
804            _templates = templates;
805        }
806    }
807    
808    private static class UnresolvedURI
809    {
810        String _rawURI;
811        String _baseURI;
812        
813        public UnresolvedURI(String rawURI, String baseURI)
814        {
815            _rawURI = rawURI;
816            _baseURI = baseURI;
817        }
818        
819        @Override
820        public int hashCode()
821        {
822            if (_rawURI.indexOf(':') > 1)
823            {
824                // rawURI is absolute
825                return _rawURI.hashCode();
826            }
827            
828            int lastPathElementPos = _baseURI.lastIndexOf('/');
829            if (lastPathElementPos == -1)
830            {
831                // this should never occur as the base should always be protocol:/....
832                return _rawURI.hashCode();
833            }
834            else
835            {
836                String uri = _baseURI.substring(0, lastPathElementPos) + "/" + _rawURI;
837                return uri.hashCode();
838            }
839        }
840        
841        @Override
842        public boolean equals(Object obj)
843        {
844            if (!(obj instanceof UnresolvedURI))
845            {
846                return false;
847            }
848            
849            UnresolvedURI unresolved = (UnresolvedURI) obj;
850            
851            if (_rawURI.indexOf(':') > 1)
852            {
853                // rawURI is absolute
854                return _rawURI.equals(unresolved._rawURI);
855            }
856            
857            int lastPathElementPos = _baseURI.lastIndexOf('/');
858            if (lastPathElementPos == -1)
859            {
860                // this should never occur as the base should always be protocol:/....
861                return false;
862            }
863            else if (unresolved._rawURI.length() <= lastPathElementPos)
864            {
865                return false;
866            }
867            else
868            {
869                String uri1 = _baseURI.substring(0, lastPathElementPos) + "/" + _rawURI;
870                String uri2 = unresolved._baseURI.substring(0, lastPathElementPos) + "/" + unresolved._rawURI;
871                
872                return uri1.equals(uri2);
873            }
874        }
875    }
876    
877    private static class ResolvedURI
878    {
879        String _resolvedURI;
880        long _timestamp;
881        
882        public ResolvedURI(String resolvedURI, long timestamp)
883        {
884            _resolvedURI = resolvedURI;
885            _timestamp = timestamp;
886        }
887    }
888    
889    private static class TemplatesCache
890    {
891        private Collection<CachedTemplates> _templatesCache;
892        private long _lastModified;
893        
894        /**
895         * Build a cache
896         * @param lastModified The last modified date
897         */
898        public TemplatesCache(long lastModified)
899        {
900            _templatesCache = new ArrayList<>();
901            _lastModified = lastModified;
902        }
903        
904        /**
905         * Get the intern collection
906         * @return The collection
907         */
908        public Collection<CachedTemplates> getCollection()
909        {
910            return _templatesCache;
911        }
912        
913        /**
914         * Get the last modified date
915         * @return the last modified date
916         */
917        public long getLastModified()
918        {
919            return _lastModified;
920        }
921    }
922}