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                true);
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                resolutionCache.put(unresolved, new ResolvedURI(resolvedURI, lastModified));
363            }
364            
365            if (!resolvedURI.equals(resolvedURIs[i]))
366            {
367                return null;
368            }
369            
370            if (lastModified == 0 || timestamps[i] == 0 || lastModified != timestamps[i])
371            {
372                outOfDate = true;
373            }
374        }
375        
376        return outOfDate ? CacheValidity.OUT_OF_DATE : CacheValidity.CACHED;
377    }
378    
379    @SuppressWarnings("unchecked")
380    private Templates _createTemplates(SAXTransformerFactory factory, Source stylesheet, XMLFilter filter) throws XSLTProcessorException
381    {
382        String id = stylesheet.getURI();
383        TraxErrorListener errorListener = new TraxErrorListener(getLogger(), id);
384
385        try
386        {
387            if (getLogger().isDebugEnabled())
388            {
389                getLogger().debug("Creating new Templates for " + id);
390            }
391
392            factory.setErrorListener(errorListener);
393
394            // Create a Templates ContentHandler to handle parsing of the
395            // stylesheet.
396            TemplatesHandler templatesHandler = factory.newTemplatesHandler();
397
398            // Set the system ID for the template handler since some
399            // TrAX implementations (XSLTC) rely on this in order to obtain
400            // a meaningful identifier for the Templates instances.
401            templatesHandler.setSystemId(id);
402            if (filter != null)
403            {
404                filter.setContentHandler(templatesHandler);
405            }
406
407            if (getLogger().isDebugEnabled())
408            {
409                getLogger().debug("Source = " + stylesheet + ", templatesHandler = " + templatesHandler);
410            }
411
412            // Process the stylesheet.
413            _sourceToSAX(stylesheet, filter != null ? (ContentHandler) filter : (ContentHandler) templatesHandler);
414
415            // Get the Templates object (generated during the parsing of
416            // the stylesheet) from the TemplatesHandler.
417            final Templates templates = templatesHandler.getTemplates();
418
419            if (null == templates)
420            {
421                throw new XSLTProcessorException("Unable to create templates for stylesheet: " + stylesheet.getURI());
422            }
423
424            // Must set base for Xalan stylesheet.
425            // Otherwise document('') in logicsheet causes NPE.
426            Class clazz = templates.getClass();
427            if (clazz.getName().equals("org.apache.xalan.templates.StylesheetRoot"))
428            {
429                Method method = clazz.getMethod("setHref", new Class[] {String.class});
430                method.invoke(templates, new Object[] {id});
431            }
432            
433            return templates;
434        }
435        catch (Exception e)
436        {
437            Throwable realEx = errorListener.getThrowable();
438            if (realEx == null)
439            {
440                realEx = e;
441            }
442
443            if (realEx instanceof RuntimeException)
444            {
445                throw (RuntimeException) realEx;
446            }
447
448            if (realEx instanceof XSLTProcessorException)
449            {
450                throw (XSLTProcessorException) realEx;
451            }
452
453            throw new XSLTProcessorException("Exception when creating Transformer from " + stylesheet.getURI(), realEx);
454        }
455    }
456    
457    private void _sourceToSAX(Source source, ContentHandler handler) throws SAXException, IOException, SourceException
458    {
459        if (source instanceof XMLizable)
460        {
461            ((XMLizable) source).toSAX(handler);
462        }
463        else
464        {
465            final InputStream inputStream = source.getInputStream();
466            final String mimeType = source.getMimeType();
467            final String systemId = source.getURI();
468            _xmlizer.toSAX(inputStream, mimeType, systemId, handler);
469        }
470    }
471
472    public void transform(final Source source, final Source stylesheet, final Parameters params, final Result result) throws XSLTProcessorException
473    {
474        try
475        {
476            if (getLogger().isDebugEnabled())
477            {
478                getLogger().debug("Transform source = " + source + ", stylesheet = " + stylesheet + ", parameters = " + params + ", result = " + result);
479            }
480            final TransformerHandler handler = getTransformerHandler(stylesheet);
481            if (params != null)
482            {
483                final Transformer transformer = handler.getTransformer();
484                transformer.clearParameters();
485                String[] names = params.getNames();
486                for (int i = names.length - 1; i >= 0; i--)
487                {
488                    transformer.setParameter(names[i], params.getParameter(names[i]));
489                }
490            }
491
492            handler.setResult(result);
493            _sourceToSAX(source, handler);
494            if (getLogger().isDebugEnabled())
495            {
496                getLogger().debug("Transform done");
497            }
498        }
499        catch (SAXException e)
500        {
501            // Unwrapping the exception will "remove" the real cause with
502            // never Xalan versions and makes the exception message unusable
503            final String message = "Error in running Transformation";
504            throw new XSLTProcessorException(message, e);
505            /*
506             * 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() ); }
507             */
508        }
509        catch (Exception e)
510        {
511            final String message = "Error in running Transformation";
512            throw new XSLTProcessorException(message, e);
513        }
514    }
515
516    /**
517     * 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.
518     * @param factoryName The name of the factory class to create
519     * @return The instance created
520     */
521    private SAXTransformerFactory _createTransformerFactory(String factoryName)
522    {
523        SAXTransformerFactory saxFactory;
524
525        if (null == factoryName)
526        {
527            saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
528        }
529        else
530        {
531            try
532            {
533                ClassLoader loader = Thread.currentThread().getContextClassLoader();
534                if (loader == null)
535                {
536                    loader = getClass().getClassLoader();
537                }
538                
539                saxFactory = (SAXTransformerFactory) loader.loadClass(factoryName).getDeclaredConstructor().newInstance();
540            }
541            catch (ClassNotFoundException cnfe)
542            {
543                getLogger().error("Cannot find the requested TrAX factory '" + factoryName + "'. Using default TrAX Transformer Factory instead.");
544                if (_factory != null)
545                {
546                    return _factory;
547                }
548                
549                saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
550            }
551            catch (ClassCastException cce)
552            {
553                getLogger().error("The indicated class '" + factoryName + "' is not a TrAX Transformer Factory. Using default TrAX Transformer Factory instead.");
554                if (_factory != null)
555                {
556                    return _factory;
557                }
558                
559                saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
560            }
561            catch (Exception e)
562            {
563                getLogger().error("Error found loading the requested TrAX Transformer 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        }
572
573        saxFactory.setErrorListener(new TraxErrorListener(getLogger(), null));
574        saxFactory.setURIResolver(this);
575
576        if (saxFactory.getClass().getName().equals("org.apache.xalan.processor.TransformerFactoryImpl"))
577        {
578            saxFactory.setAttribute("http://xml.apache.org/xalan/features/incremental", Boolean.valueOf(_incrementalProcessing));
579        }
580        // SAXON 8 will not report errors unless version warning is set to false.
581        if (saxFactory.getClass().getName().equals("net.sf.saxon.TransformerFactoryImpl"))
582        {
583            saxFactory.setAttribute("http://saxon.sf.net/feature/version-warning", Boolean.FALSE);
584        }
585
586        return saxFactory;
587    }
588
589    /**
590     * Called by the processor when it encounters an xsl:include, xsl:import, or document() function.
591     * 
592     * @param href An href attribute, which may be relative or absolute.
593     * @param base The base URI in effect when the href attribute was encountered.
594     * 
595     * @return A Source object, or null if the href cannot be resolved, and the processor should try to resolve the URI itself.
596     * 
597     * @throws TransformerException if an error occurs when trying to resolve the URI.
598     */
599    public javax.xml.transform.Source resolve(String href, String base) throws TransformerException
600    {
601        return _resolve(href, base, null, null, null, null);
602    }
603    
604    @SuppressWarnings("deprecation") 
605    private Source _resolve(String href, String base) throws IOException
606    {
607        Source xslSource = null;
608
609        if (base == null || href.indexOf(":") > 1)
610        {
611            // Null base - href must be an absolute URL
612            xslSource = _resolver.resolveURI(href);
613        }
614        else if (href.length() == 0)
615        {
616            // Empty href resolves to base
617            xslSource = _resolver.resolveURI(base);
618        }
619        else
620        {
621            // is the base a file or a real m_url
622            if (!base.startsWith("file:"))
623            {
624                int lastPathElementPos = base.lastIndexOf('/');
625                if (lastPathElementPos == -1)
626                {
627                    // this should never occur as the base should
628                    // always be protocol:/....
629                    return null; // we can't resolve this
630                }
631                else
632                {
633                    xslSource = _resolver.resolveURI(base.substring(0, lastPathElementPos) + "/" + href);
634                }
635            }
636            else
637            {
638                File parent = new File(base.substring(5));
639                File parent2 = new File(parent.getParentFile(), href);
640                xslSource = _resolver.resolveURI(parent2.toURL().toExternalForm());
641            }
642        }
643        
644        return xslSource;
645    }
646    
647    javax.xml.transform.Source _resolve(String href, String base, Collection<String> rawURIs, Collection<String> baseURIs, Collection<Long> timestamps, Collection<String> resolvedURIs)
648    {
649        if (getLogger().isDebugEnabled())
650        {
651            getLogger().debug("resolve(href = " + href + ", base = " + base + "); resolver = " + _resolver);
652        }
653
654        Source xslSource = null;
655        try
656        {
657            xslSource = _resolve(href, base);
658            
659            if (rawURIs != null)
660            {
661                rawURIs.add(href);
662            }
663            
664            if (baseURIs != null)
665            {
666                baseURIs.add(base);
667            }
668            
669            if (timestamps != null)
670            {
671                timestamps.add(xslSource.getLastModified());
672            }
673
674            if (resolvedURIs != null)
675            {
676                resolvedURIs.add(xslSource.getURI());
677            }
678
679            InputSource is = _getInputSource(xslSource);
680
681            if (getLogger().isDebugEnabled())
682            {
683                getLogger().debug("xslSource = " + xslSource + ", system id = " + xslSource.getURI());
684            }
685
686            return new StreamSource(is.getByteStream(), is.getSystemId());
687        }
688        catch (IOException ioe)
689        {
690            if (getLogger().isDebugEnabled())
691            {
692                getLogger().debug("Failed to resolve " + href + "(base = " + base + "), return null", ioe);
693            }
694
695            return null;
696        }
697        finally
698        {
699            _resolver.release(xslSource);
700        }
701    }
702
703    /**
704     * Return a new <code>InputSource</code> object that uses the <code>InputStream</code> and the system ID of the <code>Source</code> object.
705     * @param source The source concerned
706     * @return The input source
707     * @throws IOException if I/O error occurred.
708     * @throws SourceException if an error occurred.
709     */
710    private InputSource _getInputSource(final Source source) throws IOException, SourceException
711    {
712        final InputSource newObject = new InputSource(source.getInputStream());
713        newObject.setSystemId(source.getURI());
714        return newObject;
715    }
716
717    private Cache<UnresolvedURI, ResolvedURI> _getResolutionCache()
718    {
719        return _cacheManager.get(_RESOLVE_URI_CACHE_ID);
720    }
721    
722    private Cache<String, Collection<CachedTemplates>> _getTemplatesCache()
723    {
724        return _cacheManager.get(_TEMPLATES_CACHE_ID);
725    }
726    
727    /**
728     * Special {@link org.apache.excalibur.xml.xslt.XSLTProcessor.TransformerHandlerAndValidity} with information about the cache state of the underlying {@link Templates}.
729     */
730    public static class ExtendedTransformerHandlerAndValidity extends TransformerHandlerAndValidity
731    {
732        CacheValidity _cacheValidity;
733        
734        /**
735         * Constructor.
736         * @param handler the {@link TransformerHandler}.
737         * @param validity the {@link SourceValidity}.
738         * @param cacheValidity the cache status.
739         */
740        public ExtendedTransformerHandlerAndValidity(TransformerHandler handler, SourceValidity validity, CacheValidity cacheValidity)
741        {
742            super(handler, validity);
743            _cacheValidity = cacheValidity;
744        }
745        
746        /**
747         * Returns true if the underlying {@link Templates} has been taken from cache.
748         * @return true if taken from cache.
749         */
750        public boolean isFromCache()
751        {
752            return _cacheValidity == CacheValidity.CACHED;
753        }
754        
755        /**
756         * Returns the {@link CacheValidity} associated with the current stylesheet.
757         * @return the {@link CacheValidity}.
758         */
759        public CacheValidity getValidity()
760        {
761            return _cacheValidity;
762        }
763    }
764    
765    // all known Templates for a single input stylesheet
766    private static class CachedTemplates implements URIResolver
767    {
768        // root stylesheet timestamp
769        private long _lastModified;
770
771        // non-resolved included/imported URIs for the input stylesheet
772        private List<String> _rawURIs = new ArrayList<>();
773        
774        // base URIs for resolution
775        private List<String> _baseURIs = new ArrayList<>();
776        
777        // resolved URIs
778        private List<String> _resolvedURIs = new ArrayList<>();
779        
780        // last modified timestamps for resolved URIs
781        private List<Long> _timestamps = new ArrayList<>();
782        
783        // resulting templates
784        private Templates _templates;
785        
786        @ExcludeFromSizeCalculation
787        private ThreadSafeTraxProcessor _processor;
788        
789        CachedTemplates(long lastModified, ThreadSafeTraxProcessor processor)
790        {
791            _lastModified = lastModified;
792            _processor = processor;
793        }
794
795        @Override
796        public javax.xml.transform.Source resolve(String href, String base) throws TransformerException
797        {
798            return _processor._resolve(href, base, _rawURIs, _baseURIs, _timestamps, _resolvedURIs);
799        }
800        
801        long getLastModified()
802        {
803            return _lastModified;
804        }
805        
806        String[] getRawURIs()
807        {
808            return _rawURIs.toArray(new String[]{});
809        }
810        
811        String[] getBaseURIs()
812        {
813            return _baseURIs.toArray(new String[]{});
814        }
815        
816        Long[] getTimestamps()
817        {
818            return _timestamps.toArray(new Long[]{});
819        }
820        
821        String[] getResolvedURIs()
822        {
823            return _resolvedURIs.toArray(new String[]{});
824        }
825        
826        Templates getTemplates()
827        {
828            return _templates;
829        }
830        
831        void setTemplates(Templates templates)
832        {
833            _templates = templates;
834        }
835    }
836    
837    private static class UnresolvedURI
838    {
839        String _rawURI;
840        String _baseURI;
841        
842        public UnresolvedURI(String rawURI, String baseURI)
843        {
844            _rawURI = rawURI;
845            _baseURI = baseURI;
846        }
847        
848        @Override
849        public int hashCode()
850        {
851            if (_rawURI.indexOf(':') > 1)
852            {
853                // rawURI is absolute
854                return _rawURI.hashCode();
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 _rawURI.hashCode();
862            }
863            else
864            {
865                String uri = _baseURI.substring(0, lastPathElementPos) + "/" + _rawURI;
866                return uri.hashCode();
867            }
868        }
869        
870        @Override
871        public boolean equals(Object obj)
872        {
873            if (!(obj instanceof UnresolvedURI))
874            {
875                return false;
876            }
877            
878            UnresolvedURI unresolved = (UnresolvedURI) obj;
879            
880            if (_rawURI.indexOf(':') > 1)
881            {
882                // rawURI is absolute
883                return _rawURI.equals(unresolved._rawURI);
884            }
885            
886            int lastPathElementPos = _baseURI.lastIndexOf('/');
887            if (lastPathElementPos == -1)
888            {
889                // this should never occur as the base should always be protocol:/....
890                return false;
891            }
892            else if (unresolved._baseURI.length() <= lastPathElementPos)
893            {
894                return false;
895            }
896            else
897            {
898                String uri1 = _baseURI.substring(0, lastPathElementPos) + "/" + _rawURI;
899                String uri2 = unresolved._baseURI.substring(0, lastPathElementPos) + "/" + unresolved._rawURI;
900                
901                return uri1.equals(uri2);
902            }
903        }
904    }
905    
906    private static class ResolvedURI
907    {
908        String _resolvedURI;
909        long _timestamp;
910        
911        public ResolvedURI(String resolvedURI, long timestamp)
912        {
913            _resolvedURI = resolvedURI;
914            _timestamp = timestamp;
915        }
916    }
917    
918    /**
919     * The validity of the current stylesheet.
920     */
921    public enum CacheValidity
922    {
923        /** The current stylesheet is in cache and valid*/
924        CACHED,
925        /** The current styleseet is in cache but not valid anymore*/
926        OUT_OF_DATE,
927        /** The current stylesheet is not cached*/
928        NON_CACHED
929    }
930}