1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.cocoon.transformation;
19
20 import java.io.IOException;
21 import java.io.Serializable;
22 import java.lang.reflect.Method;
23 import java.util.Enumeration;
24 import java.util.HashMap;
25 import java.util.Iterator;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.Map.Entry;
29
30 import javax.xml.transform.sax.SAXResult;
31 import javax.xml.transform.sax.TransformerHandler;
32
33 import org.apache.avalon.framework.activity.Disposable;
34 import org.apache.avalon.framework.configuration.Configurable;
35 import org.apache.avalon.framework.configuration.Configuration;
36 import org.apache.avalon.framework.configuration.ConfigurationException;
37 import org.apache.avalon.framework.logger.LogEnabled;
38 import org.apache.avalon.framework.parameters.Parameters;
39 import org.apache.avalon.framework.service.ServiceException;
40 import org.apache.avalon.framework.service.ServiceManager;
41 import org.apache.avalon.framework.service.Serviceable;
42 import org.apache.cocoon.ProcessingException;
43 import org.apache.cocoon.caching.CacheableProcessingComponent;
44 import org.apache.cocoon.components.source.SourceUtil;
45 import org.apache.cocoon.components.xslt.TraxErrorListener;
46 import org.apache.cocoon.environment.Cookie;
47 import org.apache.cocoon.environment.ObjectModelHelper;
48 import org.apache.cocoon.environment.Request;
49 import org.apache.cocoon.environment.Session;
50 import org.apache.cocoon.environment.SourceResolver;
51 import org.apache.cocoon.xml.XMLConsumer;
52 import org.apache.commons.lang.BooleanUtils;
53 import org.apache.commons.lang.exception.NestableRuntimeException;
54 import org.apache.excalibur.source.Source;
55 import org.apache.excalibur.source.SourceException;
56 import org.apache.excalibur.source.SourceValidity;
57 import org.apache.excalibur.xml.xslt.XSLTProcessor;
58 import org.apache.excalibur.xml.xslt.XSLTProcessorException;
59 import org.xml.sax.SAXException;
60
61 /**
62 * @cocoon.sitemap.component.documentation
63 * The stylesheet processor
64 *
65 * @cocoon.sitemap.component.name xslt
66 * @cocoon.sitemap.component.logger sitemap.transformer.xslt
67 * @cocoon.sitemap.component.documentation.caching
68 * Uses the last modification date of the xslt document for validation
69 *
70 * @cocoon.sitemap.component.pooling.max 32
71 * <p>
72 * This Transformer is used to transform the incoming SAX stream using
73 * a TrAXProcessor. Use the following sitemap declarations to define, configure
74 * and parameterize it:
75 * </p>
76 * <b>In the map:sitemap/map:components/map:transformers:</b><br>
77 * <pre>
78 * <map:transformer name="xslt" src="org.apache.cocoon.transformation.TraxTransformer"><br>
79 * <use-request-parameters>false</use-request-parameters>
80 * <use-browser-capabilities-db>false</use-browser-capabilities-db>
81 * <use-session-info>false</use-session-info>
82 * <xslt-processor-role>xslt</xslt-processor-role>
83 * <transformer-factory>org.apache.xalan.processor.TransformerFactoryImpl</transformer-factory>
84 * <check-includes>true</check-includes>
85 * </map:transformer>
86 * </pre>
87 *
88 * The <use-request-parameter> configuration forces the transformer to make all
89 * request parameters available in the XSLT stylesheet. Note that this has
90 * implications for caching of the generated output of this transformer.<br>
91 * This property is false by default.
92 * <p>
93 * The <use-cookies> configuration forces the transformer to make all
94 * cookies from the request available in the XSLT stylesheets.
95 * Note that this has implications for caching of the generated output of this
96 * transformer.<br>
97 * This property is false by default.
98 * <p>
99 * The <use-session-info> configuration forces the transformer to make all
100 * of the session information available in the XSLT stylesheetas.<br>
101 * These infos are (boolean values are "true" or "false" strings: session-is-new,
102 * session-id-from-cookie, session-id-from-url, session-valid, session-id.<br>
103 * This property is false by default.
104 *
105 * <p>Note that this has implications for caching of the generated output of
106 * this transformer.<br>
107 *
108 *
109 * The <xslt-processor-role> configuration allows to specify the TrAX processor (defined in
110 * the cocoon.xconf) that will be used to obtain the XSLT processor. This allows to have
111 * several XSLT processors in the configuration (e.g. Xalan, XSLTC, Saxon, ...) and choose
112 * one or the other depending on the needs of stylesheet specificities.<br>
113 * If no processor is specified, this transformer will use the XSLT implementation
114 * that Cocoon uses internally.
115 *
116 * The <transformer-factory> configuration allows to specify the TrAX transformer factory
117 * implementation that will be used to obtain the XSLT processor. This is only useful for
118 * compatibility reasons. Please configure the XSLT processor in the cocoon.xconf properly
119 * and use the xslt-processor-role configuration mentioned above.
120 *
121 * The <check-includes> configuration specifies if the included stylesheets are
122 * also checked for changes during caching. If this is set to true (default), the
123 * included stylesheets are also checked for changes; if this is set to false, only
124 * the main stylesheet is checked. Setting this to false improves the performance,
125 * and should be used whenever no includes are in the stylesheet. However, if
126 * you have includes, you have to be careful when changing included stylesheets
127 * as the changes might not take effect immediately. You should touch the main
128 * stylesheet as well.
129 *
130 * <p>
131 * <b>In a map:sitemap/map:pipelines/map:pipeline:</b><br>
132 * <pre>
133 * <map:transform type="xslt" src="stylesheets/yours.xsl"><br>
134 * <parameter name="myparam" value="myvalue"/>
135 * </map:transform>
136 * </pre>
137 * All <parameter> declarations will be made available in the XSLT stylesheet as
138 * xsl:variables.
139 *
140 * @author <a href="mailto:pier@apache.org">Pierpaolo Fumagalli</a>
141 * @author <a href="mailto:dims@yahoo.com">Davanum Srinivas</a>
142 * @author <a href="mailto:cziegeler@apache.org">Carsten Ziegeler</a>
143 * @author <a href="mailto:giacomo@apache.org">Giacomo Pati</a>
144 * @author <a href="mailto:ovidiu@cup.hp.com">Ovidiu Predescu</a>
145 * @author <a href="mailto:marbut@hplb.hpl.hp.com">Mark H. Butler</a>
146 * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
147 *
148 * @version SVN $Id: TraxTransformer.java 433543 2006-08-22 06:22:54Z crossley $
149 */
150 public class TraxTransformer extends AbstractTransformer
151 implements Serviceable, Configurable, CacheableProcessingComponent, Disposable {
152
153 /** The service manager instance (protected because used by subclasses) */
154 protected ServiceManager manager;
155
156 /** The object model (protected because used by subclasses) */
157 protected Map objectModel;
158
159 /** Logicsheet parameters (protected because used by subclasses) */
160 protected Map logicSheetParameters;
161
162 /** Should we make the request parameters available in the stylesheet? (default is off) */
163 private boolean useParameters = false;
164 private boolean _useParameters = false;
165
166 /** Should we make the cookies available in the stylesheet? (default is off) */
167 private boolean useCookies = false;
168 private boolean _useCookies = false;
169
170 /** Should we info about the session available in the stylesheet? (default is off) */
171 private boolean useSessionInfo = false;
172 private boolean _useSessionInfo = false;
173
174 /** Do we check included stylesheets for changes? */
175 private boolean checkIncludes = true;
176
177 /** The trax TransformerHandler */
178 protected TransformerHandler transformerHandler;
179
180 /** The validity of the Transformer */
181 protected SourceValidity transformerValidity;
182
183 /** The Source */
184 private Source inputSource;
185 /** The parameters */
186 private Parameters par;
187 /** The source resolver */
188 private SourceResolver resolver;
189
190 /** Default source, used to create specialized transformers by configuration */
191 private String defaultSrc;
192
193 /** The XSLTProcessor */
194 private XSLTProcessor xsltProcessor;
195
196 /** Did we finish the processing (is endDocument() called) */
197 private boolean finishedDocument = false;
198
199 /** Xalan's DTMManager.getIncremental() method. See recycle() method to see what we need this for. */
200 private Method xalanDtmManagerGetIncrementalMethod;
201
202 /** Exception that might occur during setConsumer */
203 private SAXException exceptionDuringSetConsumer;
204
205 /** The error listener used by the stylesheet */
206 private TraxErrorListener errorListener;
207
208 /**
209 * Configure this transformer.
210 */
211 public void configure(Configuration conf)
212 throws ConfigurationException {
213 Configuration child;
214
215 child = conf.getChild("use-request-parameters");
216 this.useParameters = child.getValueAsBoolean(false);
217 this._useParameters = this.useParameters;
218
219 child = conf.getChild("use-cookies");
220 this.useCookies = child.getValueAsBoolean(false);
221 this._useCookies = this.useCookies;
222
223 child = conf.getChild("use-session-info");
224 this.useSessionInfo = child.getValueAsBoolean(false);
225 this._useSessionInfo = this.useSessionInfo;
226
227 child = conf.getChild("transformer-factory");
228 // traxFactory is null, if transformer-factory config is unspecified
229 final String traxFactory = child.getValue(null);
230
231 child = conf.getChild("xslt-processor-role");
232 String xsltProcessorRole = child.getValue(XSLTProcessor.ROLE);
233 if (!xsltProcessorRole.startsWith(XSLTProcessor.ROLE)) {
234 xsltProcessorRole = XSLTProcessor.ROLE + '/' + xsltProcessorRole;
235 }
236
237 child = conf.getChild("check-includes");
238 this.checkIncludes = child.getValueAsBoolean(this.checkIncludes);
239
240 child = conf.getChild("default-src",false);
241 if(child!=null) {
242 this.defaultSrc = child.getValue();
243 }
244
245 if (getLogger().isDebugEnabled()) {
246 getLogger().debug("Use parameters is " + this.useParameters);
247 getLogger().debug("Use cookies is " + this.useCookies);
248 getLogger().debug("Use session info is " + this.useSessionInfo);
249 getLogger().debug("Use TrAX Processor " + xsltProcessorRole);
250 getLogger().debug("Check for included stylesheets is " + this.checkIncludes);
251 if (traxFactory != null) {
252 getLogger().debug("Use TrAX Transformer Factory " + traxFactory);
253 } else {
254 getLogger().debug("Use default TrAX Transformer Factory.");
255 }
256 getLogger().debug("Default source = " + this.defaultSrc);
257 }
258
259 try {
260 this.xsltProcessor = (XSLTProcessor) this.manager.lookup(xsltProcessorRole);
261 if (traxFactory != null) {
262 this.xsltProcessor.setTransformerFactory(traxFactory);
263 }
264 } catch (ServiceException e) {
265 throw new ConfigurationException("Cannot load XSLT processor", e);
266 }
267
268 try {
269 // see the recyle() method to see what we need this for
270 Class dtmManagerClass = Class.forName("org.apache.xml.dtm.DTMManager");
271 xalanDtmManagerGetIncrementalMethod = dtmManagerClass.getMethod("getIncremental", null);
272 } catch (ClassNotFoundException e) {
273 // do nothing -- user does not use xalan, so we don't need the dtm manager
274 } catch (NoSuchMethodException e) {
275 throw new ConfigurationException("Was not able to get getIncremental method from Xalan's DTMManager.", e);
276 }
277 }
278
279 /**
280 * Set the current <code>ServiceManager</code> instance used by this
281 * <code>Serviceable</code>.
282 */
283 public void service(ServiceManager manager) throws ServiceException {
284 this.manager = manager;
285 }
286
287 /**
288 * Set the <code>SourceResolver</code>, the <code>Map</code> with
289 * the object model, the source and sitemap
290 * <code>Parameters</code> used to process the request.
291 */
292 public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par)
293 throws SAXException, ProcessingException, IOException {
294
295 if(src==null && defaultSrc!=null) {
296 if(getLogger().isDebugEnabled()) {
297 getLogger().debug("src is null, using default source " + defaultSrc);
298 }
299 src = defaultSrc;
300 }
301
302 if (src == null) {
303 throw new ProcessingException("Stylesheet URI can't be null");
304 }
305
306 this.par = par;
307 this.objectModel = objectModel;
308 this.resolver = resolver;
309 try {
310 this.inputSource = resolver.resolveURI(src);
311 } catch (SourceException se) {
312 throw SourceUtil.handle("Unable to resolve " + src, se);
313 }
314 _useParameters = par.getParameterAsBoolean("use-request-parameters", this.useParameters);
315 _useCookies = par.getParameterAsBoolean("use-cookies", this.useCookies);
316 _useSessionInfo = par.getParameterAsBoolean("use-session-info", this.useSessionInfo);
317 final boolean _checkIncludes = par.getParameterAsBoolean("check-includes", this.checkIncludes);
318
319 if (getLogger().isDebugEnabled()) {
320 getLogger().debug("Using stylesheet: '" + this.inputSource.getURI() + "' in " + this);
321 getLogger().debug("Use parameters is " + this._useParameters);
322 getLogger().debug("Use cookies is " + this._useCookies);
323 getLogger().debug("Use session info is " + this._useSessionInfo);
324 getLogger().debug("Check for included stylesheets is " + _checkIncludes);
325 }
326
327 // Get a Transformer Handler if we check for includes
328 // If we don't check the handler is get during setConsumer()
329 try {
330 if ( _checkIncludes ) {
331 XSLTProcessor.TransformerHandlerAndValidity handlerAndValidity =
332 this.xsltProcessor.getTransformerHandlerAndValidity(this.inputSource, null);
333 this.transformerHandler = handlerAndValidity.getTransfomerHandler();
334 this.transformerValidity = handlerAndValidity.getTransfomerValidity();
335 } else {
336 this.transformerValidity = this.inputSource.getValidity();
337 }
338 } catch (XSLTProcessorException se) {
339 throw new ProcessingException("Unable to get transformer handler for " + this.inputSource.getURI(), se);
340 }
341 }
342
343 /**
344 * Generate the unique key.
345 * This key must be unique inside the space of this component.
346 *
347 * @return The generated key hashes the src
348 */
349 public Serializable getKey() {
350 Map map = getLogicSheetParameters();
351 if (map == null) {
352 return this.inputSource.getURI();
353 }
354
355 StringBuffer sb = new StringBuffer();
356 sb.append(this.inputSource.getURI());
357 Set entries = map.entrySet();
358 for(Iterator i=entries.iterator(); i.hasNext();){
359 sb.append(';');
360 Map.Entry entry = (Map.Entry)i.next();
361 sb.append(entry.getKey());
362 sb.append('=');
363 sb.append(entry.getValue());
364 }
365 return sb.toString();
366 }
367
368 /**
369 * Generate the validity object.
370 *
371 * @return The generated validity object or <code>null</code> if the
372 * component is currently not cacheable.
373 */
374 public SourceValidity getValidity() {
375 //
376 // VG: Key is generated using parameter/value pairs,
377 // so this information does not need to be verified again
378 // (if parameter added/removed or value changed, key should
379 // change also), only stylesheet's validity is included.
380 //
381 return this.transformerValidity;
382 }
383
384 /**
385 * Set the <code>XMLConsumer</code> that will receive XML data.
386 */
387 public void setConsumer(XMLConsumer consumer) {
388
389 if ( this.transformerHandler == null ) {
390 try {
391 this.transformerHandler = this.xsltProcessor.getTransformerHandler(this.inputSource);
392 } catch (XSLTProcessorException se) {
393 // the exception will be thrown during startDocument()
394 this.exceptionDuringSetConsumer =
395 new SAXException("Unable to get transformer handler for " + this.inputSource.getURI(), se);
396 return;
397 }
398 }
399 final Map map = getLogicSheetParameters();
400 if (map != null) {
401 final javax.xml.transform.Transformer transformer = this.transformerHandler.getTransformer();
402 final Iterator iterator = map.entrySet().iterator();
403 while (iterator.hasNext()) {
404 final Map.Entry entry = (Entry) iterator.next();
405 transformer.setParameter((String)entry.getKey(), entry.getValue());
406 }
407 }
408
409 super.setContentHandler(this.transformerHandler);
410 super.setLexicalHandler(this.transformerHandler);
411
412 if (this.transformerHandler instanceof LogEnabled) {
413 ((LogEnabled)this.transformerHandler).enableLogging(getLogger());
414 }
415 // According to TrAX specs, all TransformerHandlers are LexicalHandlers
416 final SAXResult result = new SAXResult(consumer);
417 result.setLexicalHandler(consumer);
418 this.transformerHandler.setResult(result);
419
420 this.errorListener = new TraxErrorListener(getLogger(), this.inputSource.getURI());
421 this.transformerHandler.getTransformer().setErrorListener(this.errorListener);
422 }
423
424 /**
425 * Get the parameters for the logicsheet
426 */
427 protected Map getLogicSheetParameters() {
428 if (this.logicSheetParameters != null) {
429 return this.logicSheetParameters;
430 }
431 HashMap map = null;
432 if (par != null) {
433 String[] params = par.getNames();
434 if (params != null) {
435 for(int i = 0; i < params.length; i++) {
436 String name = params[i];
437 if (isValidXSLTParameterName(name)) {
438 String value = par.getParameter(name,null);
439 if (value != null) {
440 if (map == null) {
441 map = new HashMap(params.length);
442 }
443 map.put(name,value);
444 }
445 }
446 }
447 }
448 }
449
450 if (this._useParameters) {
451 Request request = ObjectModelHelper.getRequest(objectModel);
452
453 Enumeration parameters = request.getParameterNames();
454 if (parameters != null) {
455 while (parameters.hasMoreElements()) {
456 String name = (String) parameters.nextElement();
457 if (isValidXSLTParameterName(name)) {
458 String value = request.getParameter(name);
459 if (map == null) {
460 map = new HashMap();
461 }
462 map.put(name,value);
463 }
464 }
465 }
466 }
467
468 if (this._useSessionInfo) {
469 final Request request = ObjectModelHelper.getRequest(objectModel);
470 if (map == null) {
471 map = new HashMap(6);
472 }
473
474 final Session session = request.getSession(false);
475 if (session != null) {
476 map.put("session-available", "true");
477 map.put("session-is-new", BooleanUtils.toStringTrueFalse(session.isNew()));
478 map.put("session-id-from-cookie", BooleanUtils.toStringTrueFalse(request.isRequestedSessionIdFromCookie()));
479 map.put("session-id-from-url", BooleanUtils.toStringTrueFalse(request.isRequestedSessionIdFromURL()));
480 map.put("session-valid", BooleanUtils.toStringTrueFalse(request.isRequestedSessionIdValid()));
481 map.put("session-id", session.getId());
482 } else {
483 map.put("session-available", "false");
484 }
485 }
486
487 if (this._useCookies) {
488 Request request = ObjectModelHelper.getRequest(objectModel);
489 Cookie cookies[] = request.getCookies();
490 if (cookies != null) {
491 for (int i = 0; i < cookies.length; i++) {
492 String name = cookies[i].getName();
493 if (isValidXSLTParameterName(name)) {
494 String value = cookies[i].getValue();
495 if (map == null) {
496 map = new HashMap(cookies.length);
497 }
498 map.put(name,value);
499 }
500 }
501 }
502 }
503 this.logicSheetParameters = map;
504 return this.logicSheetParameters;
505 }
506
507 /**
508 * Test if the name is a valid parameter name for XSLT
509 */
510 static boolean isValidXSLTParameterName(String name) {
511 if (name.length() == 0) {
512 return false;
513 }
514
515 char c = name.charAt(0);
516 if (!(Character.isLetter(c) || c == '_')) {
517 return false;
518 }
519
520 for (int i = name.length()-1; i > 1; i--) {
521 c = name.charAt(i);
522 if (!(Character.isLetterOrDigit(c) ||
523 c == '-' ||
524 c == '_' ||
525 c == '.')) {
526 return false;
527 }
528 }
529 return true;
530 }
531
532 /**
533 * Disposable
534 */
535 public void dispose() {
536 if ( this.manager != null ) {
537 this.manager.release(this.xsltProcessor);
538 this.xsltProcessor = null;
539 this.manager = null;
540 }
541 }
542
543 /**
544 * Recyclable
545 */
546 public void recycle() {
547 this.objectModel = null;
548 if (this.inputSource != null) {
549 this.resolver.release(this.inputSource);
550 this.inputSource = null;
551 }
552 this.resolver = null;
553 this.par = null;
554 if (!this.finishedDocument && transformerHandler != null) {
555 // This situation will only occur if an exception occured during pipeline execution.
556 // If Xalan is used in incremental mode, it is important that endDocument is called, otherwise
557 // the thread on which it runs the transformation will keep waiting.
558 // However, calling endDocument will cause the pipeline to continue executing, and thus the
559 // serializer will write output to the outputstream after what's already there (the error page),
560 // see also bug 13186.
561 if (xalanDtmManagerGetIncrementalMethod != null
562 && transformerHandler.getClass().getName().equals("org.apache.xalan.transformer.TransformerHandlerImpl")) {
563 try {
564 final boolean incremental = ((Boolean)xalanDtmManagerGetIncrementalMethod.invoke(null, null)).booleanValue();
565 if (incremental) {
566 super.endDocument();
567 }
568 } catch (Exception ignore) {}
569 }
570 }
571 this.finishedDocument = true;
572 this.logicSheetParameters = null;
573 this.transformerHandler = null;
574 this.transformerValidity = null;
575 this.exceptionDuringSetConsumer = null;
576 this.errorListener = null;
577 super.recycle();
578 }
579
580 /**
581 * Fix for stopping hanging threads of Xalan
582 */
583 public void endDocument()
584 throws SAXException {
585 try {
586 super.endDocument();
587 } catch(Exception e) {
588
589 Throwable realEx = this.errorListener.getThrowable();
590 if (realEx == null) realEx = e;
591
592 if (realEx instanceof RuntimeException) {
593 throw (RuntimeException)realEx;
594 }
595
596 if (realEx instanceof SAXException) {
597 throw (SAXException)realEx;
598 }
599
600 if (realEx instanceof Error) {
601 throw (Error)realEx;
602 }
603
604 throw new NestableRuntimeException(realEx);
605 }
606 this.finishedDocument = true;
607 }
608
609 /* (non-Javadoc)
610 * @see org.xml.sax.ContentHandler#startDocument()
611 */
612 public void startDocument() throws SAXException {
613 // did an exception occur during setConsumer?
614 // if so, throw it here
615 if ( this.exceptionDuringSetConsumer != null ) {
616 throw this.exceptionDuringSetConsumer;
617 }
618 this.finishedDocument = false;
619 super.startDocument();
620 }
621 }