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