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