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