001/*
002 *  Copyright 2013 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.runtime.cocoon;
017
018import java.io.File;
019import java.io.IOException;
020import java.io.InputStream;
021import java.lang.reflect.Method;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.HashMap;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Map;
028
029import javax.xml.transform.Result;
030import javax.xml.transform.Templates;
031import javax.xml.transform.Transformer;
032import javax.xml.transform.TransformerConfigurationException;
033import javax.xml.transform.TransformerException;
034import javax.xml.transform.TransformerFactory;
035import javax.xml.transform.URIResolver;
036import javax.xml.transform.sax.SAXTransformerFactory;
037import javax.xml.transform.sax.TemplatesHandler;
038import javax.xml.transform.sax.TransformerHandler;
039import javax.xml.transform.stream.StreamSource;
040
041import org.apache.avalon.framework.activity.Disposable;
042import org.apache.avalon.framework.activity.Initializable;
043import org.apache.avalon.framework.context.Context;
044import org.apache.avalon.framework.context.ContextException;
045import org.apache.avalon.framework.context.Contextualizable;
046import org.apache.avalon.framework.logger.AbstractLogEnabled;
047import org.apache.avalon.framework.parameters.ParameterException;
048import org.apache.avalon.framework.parameters.Parameterizable;
049import org.apache.avalon.framework.parameters.Parameters;
050import org.apache.avalon.framework.service.ServiceException;
051import org.apache.avalon.framework.service.ServiceManager;
052import org.apache.avalon.framework.service.Serviceable;
053import org.apache.cocoon.components.ContextHelper;
054import org.apache.cocoon.components.xslt.TraxErrorListener;
055import org.apache.cocoon.environment.Request;
056import org.apache.commons.lang.BooleanUtils;
057import org.apache.commons.lang3.tuple.Pair;
058import org.apache.excalibur.source.Source;
059import org.apache.excalibur.source.SourceException;
060import org.apache.excalibur.source.SourceResolver;
061import org.apache.excalibur.source.SourceValidity;
062import org.apache.excalibur.xml.sax.XMLizable;
063import org.apache.excalibur.xml.xslt.XSLTProcessor;
064import org.apache.excalibur.xml.xslt.XSLTProcessorException;
065import org.apache.excalibur.xmlizer.XMLizer;
066import org.xml.sax.ContentHandler;
067import org.xml.sax.InputSource;
068import org.xml.sax.SAXException;
069import org.xml.sax.XMLFilter;
070
071/**
072 * Adaptation of Excalibur's XSLTProcessor implementation to allow for better error reporting. This implementation is also threadsafe.<br>
073 * It also handles a {@link Templates} cache, for performance purpose.
074 */
075public class ThreadSafeTraxProcessor extends AbstractLogEnabled implements XSLTProcessor, Serviceable, Initializable, Disposable, Parameterizable, URIResolver, Contextualizable
076{
077    private static final String __URI_CACHE_ATTR = "cache.xslt.resolvedURIs";
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    private XMLizer _xmlizer;
092
093    /** The ServiceManager */
094    private ServiceManager _manager;
095    
096    private Context _context;
097    
098    // the XSLT cache
099    private Map<String, Collection<CachedTemplates>> _templatesCache = new HashMap<>();
100    
101    @Override
102    public void contextualize(Context context) throws ContextException
103    {
104        _context = context;
105    }
106
107    /**
108     * Compose. Try to get the store
109     */
110    public void service(final ServiceManager manager) throws ServiceException
111    {
112        _manager = manager;
113        _xmlizer = (XMLizer) _manager.lookup(XMLizer.ROLE);
114        _resolver = (SourceResolver) _manager.lookup(SourceResolver.ROLE);
115    }
116
117    /**
118     * Initialize
119     */
120    public void initialize() throws Exception
121    {
122        _factory = _createTransformerFactory(_transformerFactory);
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        _templatesCache.clear();
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 = _templatesCache.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                _templatesCache.put(uri, cachedTemplatesForUri);
256            }
257            
258            CachedTemplates cachedTemplates = new CachedTemplates(lastModified);
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            return Pair.of(templates, validity);
288        }
289    }
290    
291    private Pair<CachedTemplates, CacheValidity> _getCachedTemplates(Collection<CachedTemplates> cachedTemplates, Source stylesheet, Map<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    @SuppressWarnings("unchecked")
323    private Map<UnresolvedURI, ResolvedURI> _getResolutionCache()
324    {
325        Request request = null;
326        try
327        {
328            request = ContextHelper.getRequest(_context);
329        }
330        catch (Exception e)
331        {
332            // ignore, there's simply no current request
333        }
334        
335        // very simple cache for storing raw/resolved URI pairs to avoid unnecessary calls to SourceResolver 
336        Map<UnresolvedURI, ResolvedURI> resolutionCache = null;
337        if (request != null)
338        {
339            resolutionCache = (Map<UnresolvedURI, ResolvedURI>) request.getAttribute(__URI_CACHE_ATTR);
340            if (resolutionCache == null)
341            {
342                resolutionCache = new HashMap<>();
343                request.setAttribute(__URI_CACHE_ATTR, resolutionCache);
344            }
345        }
346        else
347        {
348            resolutionCache = new HashMap<>();
349        }
350        
351        return resolutionCache;
352    }
353    
354    private CacheValidity _isValid(CachedTemplates templates, Source stylesheet, Map<UnresolvedURI, ResolvedURI> resolutionCache) throws IOException
355    {
356        // the current Templates object is valid if and only if the resolution of raw URIs correspond to stored resolved URIs
357        String[] rawURIs = templates.getRawURIs();
358        String[] baseURIs = templates.getBaseURIs();
359        String[] resolvedURIs = templates.getResolvedURIs();
360        Long[] timestamps = templates.getTimestamps();
361        
362        if (templates.getLastModified() != stylesheet.getLastModified())
363        {
364            return CacheValidity.OUT_OF_DATE;
365        }
366        
367        boolean outOfDate = false;
368        for (int i = 0; i < rawURIs.length; i++)
369        {
370            // small optimization in the case where the same resolution has already been requested in the current context
371            UnresolvedURI unresolved = new UnresolvedURI(rawURIs[i], baseURIs[i]);
372            ResolvedURI resolved = resolutionCache.get(new UnresolvedURI(rawURIs[i], baseURIs[i]));
373            
374            String resolvedURI;
375            long lastModified;
376            if (resolved != null)
377            {
378                resolvedURI = resolved._resolvedURI;
379                lastModified = resolved._timestamp;
380            }
381            else
382            {
383                Source src = _resolve(rawURIs[i], baseURIs[i]);
384                resolvedURI = src.getURI();
385                lastModified = src.getLastModified();
386                resolutionCache.put(unresolved, new ResolvedURI(resolvedURI, lastModified));
387            }
388            
389            if (!resolvedURI.equals(resolvedURIs[i]))
390            {
391                return null;
392            }
393            
394            if (lastModified == 0 || timestamps[i] == 0 || lastModified != timestamps[i])
395            {
396                outOfDate = true;
397            }
398        }
399        
400        return outOfDate ? CacheValidity.OUT_OF_DATE : CacheValidity.CACHED;
401    }
402    
403    @SuppressWarnings("unchecked")
404    private Templates _createTemplates(SAXTransformerFactory factory, Source stylesheet, XMLFilter filter) throws XSLTProcessorException
405    {
406        String id = stylesheet.getURI();
407        TraxErrorListener errorListener = new TraxErrorListener(getLogger(), id);
408
409        try
410        {
411            if (getLogger().isDebugEnabled())
412            {
413                getLogger().debug("Creating new Templates for " + id);
414            }
415
416            factory.setErrorListener(errorListener);
417
418            // Create a Templates ContentHandler to handle parsing of the
419            // stylesheet.
420            TemplatesHandler templatesHandler = factory.newTemplatesHandler();
421
422            // Set the system ID for the template handler since some
423            // TrAX implementations (XSLTC) rely on this in order to obtain
424            // a meaningful identifier for the Templates instances.
425            templatesHandler.setSystemId(id);
426            if (filter != null)
427            {
428                filter.setContentHandler(templatesHandler);
429            }
430
431            if (getLogger().isDebugEnabled())
432            {
433                getLogger().debug("Source = " + stylesheet + ", templatesHandler = " + templatesHandler);
434            }
435
436            // Process the stylesheet.
437            _sourceToSAX(stylesheet, filter != null ? (ContentHandler) filter : (ContentHandler) templatesHandler);
438
439            // Get the Templates object (generated during the parsing of
440            // the stylesheet) from the TemplatesHandler.
441            final Templates templates = templatesHandler.getTemplates();
442
443            if (null == templates)
444            {
445                throw new XSLTProcessorException("Unable to create templates for stylesheet: " + stylesheet.getURI());
446            }
447
448            // Must set base for Xalan stylesheet.
449            // Otherwise document('') in logicsheet causes NPE.
450            Class clazz = templates.getClass();
451            if (clazz.getName().equals("org.apache.xalan.templates.StylesheetRoot"))
452            {
453                Method method = clazz.getMethod("setHref", new Class[] {String.class});
454                method.invoke(templates, new Object[] {id});
455            }
456            
457            return templates;
458        }
459        catch (Exception e)
460        {
461            Throwable realEx = errorListener.getThrowable();
462            if (realEx == null)
463            {
464                realEx = e;
465            }
466
467            if (realEx instanceof RuntimeException)
468            {
469                throw (RuntimeException) realEx;
470            }
471
472            if (realEx instanceof XSLTProcessorException)
473            {
474                throw (XSLTProcessorException) realEx;
475            }
476
477            throw new XSLTProcessorException("Exception when creating Transformer from " + stylesheet.getURI(), realEx);
478        }
479    }
480    
481    private void _sourceToSAX(Source source, ContentHandler handler) throws SAXException, IOException, SourceException
482    {
483        if (source instanceof XMLizable)
484        {
485            ((XMLizable) source).toSAX(handler);
486        }
487        else
488        {
489            final InputStream inputStream = source.getInputStream();
490            final String mimeType = source.getMimeType();
491            final String systemId = source.getURI();
492            _xmlizer.toSAX(inputStream, mimeType, systemId, handler);
493        }
494    }
495
496    public void transform(final Source source, final Source stylesheet, final Parameters params, final Result result) throws XSLTProcessorException
497    {
498        try
499        {
500            if (getLogger().isDebugEnabled())
501            {
502                getLogger().debug("Transform source = " + source + ", stylesheet = " + stylesheet + ", parameters = " + params + ", result = " + result);
503            }
504            final TransformerHandler handler = getTransformerHandler(stylesheet);
505            if (params != null)
506            {
507                final Transformer transformer = handler.getTransformer();
508                transformer.clearParameters();
509                String[] names = params.getNames();
510                for (int i = names.length - 1; i >= 0; i--)
511                {
512                    transformer.setParameter(names[i], params.getParameter(names[i]));
513                }
514            }
515
516            handler.setResult(result);
517            _sourceToSAX(source, handler);
518            if (getLogger().isDebugEnabled())
519            {
520                getLogger().debug("Transform done");
521            }
522        }
523        catch (SAXException e)
524        {
525            // Unwrapping the exception will "remove" the real cause with
526            // never Xalan versions and makes the exception message unusable
527            final String message = "Error in running Transformation";
528            throw new XSLTProcessorException(message, e);
529            /*
530             * 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() ); }
531             */
532        }
533        catch (Exception e)
534        {
535            final String message = "Error in running Transformation";
536            throw new XSLTProcessorException(message, e);
537        }
538    }
539
540    /**
541     * 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.
542     * @param factoryName The name of the factory class to create
543     * @return The instance created
544     */
545    private SAXTransformerFactory _createTransformerFactory(String factoryName)
546    {
547        SAXTransformerFactory saxFactory;
548
549        if (null == factoryName)
550        {
551            saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
552        }
553        else
554        {
555            try
556            {
557                ClassLoader loader = Thread.currentThread().getContextClassLoader();
558                if (loader == null)
559                {
560                    loader = getClass().getClassLoader();
561                }
562                
563                saxFactory = (SAXTransformerFactory) loader.loadClass(factoryName).newInstance();
564            }
565            catch (ClassNotFoundException cnfe)
566            {
567                getLogger().error("Cannot find the requested TrAX factory '" + factoryName + "'. Using default TrAX Transformer Factory instead.");
568                if (_factory != null)
569                {
570                    return _factory;
571                }
572                
573                saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
574            }
575            catch (ClassCastException cce)
576            {
577                getLogger().error("The indicated class '" + factoryName + "' is not a TrAX Transformer Factory. Using default TrAX Transformer Factory instead.");
578                if (_factory != null)
579                {
580                    return _factory;
581                }
582                
583                saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
584            }
585            catch (Exception e)
586            {
587                getLogger().error("Error found loading the requested TrAX Transformer Factory '" + factoryName + "'. Using default TrAX Transformer Factory instead.");
588                if (_factory != null)
589                {
590                    return _factory;
591                }
592                
593                saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
594            }
595        }
596
597        saxFactory.setErrorListener(new TraxErrorListener(getLogger(), null));
598        saxFactory.setURIResolver(this);
599
600        if (saxFactory.getClass().getName().equals("org.apache.xalan.processor.TransformerFactoryImpl"))
601        {
602            saxFactory.setAttribute("http://xml.apache.org/xalan/features/incremental", BooleanUtils.toBooleanObject(_incrementalProcessing));
603        }
604        // SAXON 8 will not report errors unless version warning is set to false.
605        if (saxFactory.getClass().getName().equals("net.sf.saxon.TransformerFactoryImpl"))
606        {
607            saxFactory.setAttribute("http://saxon.sf.net/feature/version-warning", Boolean.FALSE);
608        }
609
610        return saxFactory;
611    }
612
613    /**
614     * Called by the processor when it encounters an xsl:include, xsl:import, or document() function.
615     * 
616     * @param href An href attribute, which may be relative or absolute.
617     * @param base The base URI in effect when the href attribute was encountered.
618     * 
619     * @return A Source object, or null if the href cannot be resolved, and the processor should try to resolve the URI itself.
620     * 
621     * @throws TransformerException if an error occurs when trying to resolve the URI.
622     */
623    public javax.xml.transform.Source resolve(String href, String base) throws TransformerException
624    {
625        return _resolve(href, base, null, null, null, null);
626    }
627    
628    @SuppressWarnings("deprecation") 
629    private Source _resolve(String href, String base) throws IOException
630    {
631        Source xslSource = null;
632
633        if (base == null || href.indexOf(":") > 1)
634        {
635            // Null base - href must be an absolute URL
636            xslSource = _resolver.resolveURI(href);
637        }
638        else if (href.length() == 0)
639        {
640            // Empty href resolves to base
641            xslSource = _resolver.resolveURI(base);
642        }
643        else
644        {
645            // is the base a file or a real m_url
646            if (!base.startsWith("file:"))
647            {
648                int lastPathElementPos = base.lastIndexOf('/');
649                if (lastPathElementPos == -1)
650                {
651                    // this should never occur as the base should
652                    // always be protocol:/....
653                    return null; // we can't resolve this
654                }
655                else
656                {
657                    xslSource = _resolver.resolveURI(base.substring(0, lastPathElementPos) + "/" + href);
658                }
659            }
660            else
661            {
662                File parent = new File(base.substring(5));
663                File parent2 = new File(parent.getParentFile(), href);
664                xslSource = _resolver.resolveURI(parent2.toURL().toExternalForm());
665            }
666        }
667        
668        return xslSource;
669    }
670    
671    javax.xml.transform.Source _resolve(String href, String base, Collection<String> rawURIs, Collection<String> baseURIs, Collection<Long> timestamps, Collection<String> resolvedURIs)
672    {
673        if (getLogger().isDebugEnabled())
674        {
675            getLogger().debug("resolve(href = " + href + ", base = " + base + "); resolver = " + _resolver);
676        }
677
678        Source xslSource = null;
679        try
680        {
681            xslSource = _resolve(href, base);
682            
683            if (rawURIs != null)
684            {
685                rawURIs.add(href);
686            }
687            
688            if (baseURIs != null)
689            {
690                baseURIs.add(base);
691            }
692            
693            if (timestamps != null)
694            {
695                timestamps.add(xslSource.getLastModified());
696            }
697
698            if (resolvedURIs != null)
699            {
700                resolvedURIs.add(xslSource.getURI());
701            }
702
703            InputSource is = _getInputSource(xslSource);
704
705            if (getLogger().isDebugEnabled())
706            {
707                getLogger().debug("xslSource = " + xslSource + ", system id = " + xslSource.getURI());
708            }
709
710            return new StreamSource(is.getByteStream(), is.getSystemId());
711        }
712        catch (IOException ioe)
713        {
714            if (getLogger().isDebugEnabled())
715            {
716                getLogger().debug("Failed to resolve " + href + "(base = " + base + "), return null", ioe);
717            }
718
719            return null;
720        }
721        finally
722        {
723            _resolver.release(xslSource);
724        }
725    }
726
727    /**
728     * Return a new <code>InputSource</code> object that uses the <code>InputStream</code> and the system ID of the <code>Source</code> object.
729     * @param source The source concerned
730     * @return The input source
731     * @throws IOException if I/O error occurred.
732     * @throws SourceException if an error occurred.
733     */
734    private InputSource _getInputSource(final Source source) throws IOException, SourceException
735    {
736        final InputSource newObject = new InputSource(source.getInputStream());
737        newObject.setSystemId(source.getURI());
738        return newObject;
739    }
740
741    /**
742     * Special {@link org.apache.excalibur.xml.xslt.XSLTProcessor.TransformerHandlerAndValidity} with information about the cache state of the underlying {@link Templates}.
743     */
744    public static class ExtendedTransformerHandlerAndValidity extends TransformerHandlerAndValidity
745    {
746        CacheValidity _cacheValidity;
747        
748        /**
749         * Constructor.
750         * @param handler the {@link TransformerHandler}.
751         * @param validity the {@link SourceValidity}.
752         * @param cacheValidity the cache status.
753         */
754        public ExtendedTransformerHandlerAndValidity(TransformerHandler handler, SourceValidity validity, CacheValidity cacheValidity)
755        {
756            super(handler, validity);
757            _cacheValidity = cacheValidity;
758        }
759        
760        /**
761         * Returns true if the underlying {@link Templates} has been taken from cache.
762         * @return true if taken from cache.
763         */
764        public boolean isFromCache()
765        {
766            return _cacheValidity == CacheValidity.CACHED;
767        }
768        
769        /**
770         * Returns the {@link CacheValidity} associated with the current stylesheet.
771         * @return the {@link CacheValidity}.
772         */
773        public CacheValidity getValidity()
774        {
775            return _cacheValidity;
776        }
777    }
778    
779    // all known Templates for a single input stylesheet
780    private class CachedTemplates implements URIResolver
781    {
782        // root stylesheet timestamp
783        private long _lastModified;
784
785        // non-resolved included/imported URIs for the input stylesheet
786        private List<String> _rawURIs = new ArrayList<>();
787        
788        // base URIs for resolution
789        private List<String> _baseURIs = new ArrayList<>();
790        
791        // resolved URIs
792        private List<String> _resolvedURIs = new ArrayList<>();
793        
794        // last modified timestamps for resolved URIs
795        private List<Long> _timestamps = new ArrayList<>();
796        
797        // resulting templates
798        private Templates _templates;
799        
800        CachedTemplates(long lastModified)
801        {
802            _lastModified = lastModified;
803        }
804
805        @Override
806        public javax.xml.transform.Source resolve(String href, String base) throws TransformerException
807        {
808            return _resolve(href, base, _rawURIs, _baseURIs, _timestamps, _resolvedURIs);
809        }
810        
811        long getLastModified()
812        {
813            return _lastModified;
814        }
815        
816        String[] getRawURIs()
817        {
818            return _rawURIs.toArray(new String[]{});
819        }
820        
821        String[] getBaseURIs()
822        {
823            return _baseURIs.toArray(new String[]{});
824        }
825        
826        Long[] getTimestamps()
827        {
828            return _timestamps.toArray(new Long[]{});
829        }
830        
831        String[] getResolvedURIs()
832        {
833            return _resolvedURIs.toArray(new String[]{});
834        }
835        
836        Templates getTemplates()
837        {
838            return _templates;
839        }
840        
841        void setTemplates(Templates templates)
842        {
843            _templates = templates;
844        }
845    }
846    
847    private static class UnresolvedURI
848    {
849        String _rawURI;
850        String _baseURI;
851        
852        public UnresolvedURI(String rawURI, String baseURI)
853        {
854            _rawURI = rawURI;
855            _baseURI = baseURI;
856        }
857        
858        @Override
859        public int hashCode()
860        {
861            if (_rawURI.indexOf(':') > 1)
862            {
863                // rawURI is absolute
864                return _rawURI.hashCode();
865            }
866            
867            int lastPathElementPos = _baseURI.lastIndexOf('/');
868            if (lastPathElementPos == -1)
869            {
870                // this should never occur as the base should always be protocol:/....
871                return _rawURI.hashCode();
872            }
873            else
874            {
875                String uri = _baseURI.substring(0, lastPathElementPos) + "/" + _rawURI;
876                return uri.hashCode();
877            }
878        }
879        
880        @Override
881        public boolean equals(Object obj)
882        {
883            if (!(obj instanceof UnresolvedURI))
884            {
885                return false;
886            }
887            
888            UnresolvedURI unresolved = (UnresolvedURI) obj;
889            
890            if (_rawURI.indexOf(':') > 1)
891            {
892                // rawURI is absolute
893                return _rawURI.equals(unresolved._rawURI);
894            }
895            
896            int lastPathElementPos = _baseURI.lastIndexOf('/');
897            if (lastPathElementPos == -1)
898            {
899                // this should never occur as the base should always be protocol:/....
900                return false;
901            }
902            else if (unresolved._rawURI.length() <= lastPathElementPos)
903            {
904                return false;
905            }
906            else
907            {
908                String uri1 = _baseURI.substring(0, lastPathElementPos) + "/" + _rawURI;
909                String uri2 = unresolved._baseURI.substring(0, lastPathElementPos) + "/" + unresolved._rawURI;
910                
911                return uri1.equals(uri2);
912            }
913        }
914    }
915    
916    private static class ResolvedURI
917    {
918        String _resolvedURI;
919        long _timestamp;
920        
921        public ResolvedURI(String resolvedURI, long timestamp)
922        {
923            _resolvedURI = resolvedURI;
924            _timestamp = timestamp;
925        }
926    }
927    
928    /**
929     * The validity of the current stylesheet.
930     */
931    public enum CacheValidity
932    {
933        /** The current stylesheet is in cache and valid*/
934        CACHED,
935        /** The current styleseet is in cache but not valid anymore*/
936        OUT_OF_DATE,
937        /** The current stylesheet is not cached*/
938        NON_CACHED
939    }
940}