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