001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package org.apache.cocoon.transformation;
019:
020: import java.io.IOException;
021: import java.io.Serializable;
022: import java.lang.reflect.Method;
023: import java.util.Enumeration;
024: import java.util.HashMap;
025: import java.util.Iterator;
026: import java.util.Map;
027: import java.util.Set;
028: import java.util.Map.Entry;
029:
030: import javax.xml.transform.sax.SAXResult;
031: import javax.xml.transform.sax.TransformerHandler;
032:
033: import org.apache.avalon.framework.activity.Disposable;
034: import org.apache.avalon.framework.configuration.Configurable;
035: import org.apache.avalon.framework.configuration.Configuration;
036: import org.apache.avalon.framework.configuration.ConfigurationException;
037: import org.apache.avalon.framework.logger.LogEnabled;
038: import org.apache.avalon.framework.parameters.Parameters;
039: import org.apache.avalon.framework.service.ServiceException;
040: import org.apache.avalon.framework.service.ServiceManager;
041: import org.apache.avalon.framework.service.Serviceable;
042: import org.apache.cocoon.ProcessingException;
043: import org.apache.cocoon.caching.CacheableProcessingComponent;
044: import org.apache.cocoon.components.source.SourceUtil;
045: import org.apache.cocoon.components.xslt.TraxErrorListener;
046: import org.apache.cocoon.environment.Cookie;
047: import org.apache.cocoon.environment.ObjectModelHelper;
048: import org.apache.cocoon.environment.Request;
049: import org.apache.cocoon.environment.Session;
050: import org.apache.cocoon.environment.SourceResolver;
051: import org.apache.cocoon.xml.XMLConsumer;
052: import org.apache.commons.lang.BooleanUtils;
053: import org.apache.commons.lang.exception.NestableRuntimeException;
054: import org.apache.excalibur.source.Source;
055: import org.apache.excalibur.source.SourceException;
056: import org.apache.excalibur.source.SourceValidity;
057: import org.apache.excalibur.xml.xslt.XSLTProcessor;
058: import org.apache.excalibur.xml.xslt.XSLTProcessorException;
059: import org.xml.sax.SAXException;
060:
061: /**
062: * @cocoon.sitemap.component.documentation
063: * The stylesheet processor
064: *
065: * @cocoon.sitemap.component.name xslt
066: * @cocoon.sitemap.component.logger sitemap.transformer.xslt
067: * @cocoon.sitemap.component.documentation.caching
068: * Uses the last modification date of the xslt document for validation
069: *
070: * @cocoon.sitemap.component.pooling.max 32
071: * <p>
072: * This Transformer is used to transform the incoming SAX stream using
073: * a TrAXProcessor. Use the following sitemap declarations to define, configure
074: * and parameterize it:
075: * </p>
076: * <b>In the map:sitemap/map:components/map:transformers:</b><br>
077: * <pre>
078: * <map:transformer name="xslt" src="org.apache.cocoon.transformation.TraxTransformer"><br>
079: * <use-request-parameters>false</use-request-parameters>
080: * <use-browser-capabilities-db>false</use-browser-capabilities-db>
081: * <use-session-info>false</use-session-info>
082: * <xslt-processor-role>xslt</xslt-processor-role>
083: * <transformer-factory>org.apache.xalan.processor.TransformerFactoryImpl</transformer-factory>
084: * <check-includes>true</check-includes>
085: * </map:transformer>
086: * </pre>
087: *
088: * The <use-request-parameter> configuration forces the transformer to make all
089: * request parameters available in the XSLT stylesheet. Note that this has
090: * implications for caching of the generated output of this transformer.<br>
091: * This property is false by default.
092: * <p>
093: * The <use-cookies> configuration forces the transformer to make all
094: * cookies from the request available in the XSLT stylesheets.
095: * Note that this has implications for caching of the generated output of this
096: * transformer.<br>
097: * This property is false by default.
098: * <p>
099: * 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 implements
151: Serviceable, Configurable, CacheableProcessingComponent,
152: Disposable {
153:
154: /** The service manager instance (protected because used by subclasses) */
155: protected ServiceManager manager;
156:
157: /** The object model (protected because used by subclasses) */
158: protected Map objectModel;
159:
160: /** Logicsheet parameters (protected because used by subclasses) */
161: protected Map logicSheetParameters;
162:
163: /** Should we make the request parameters available in the stylesheet? (default is off) */
164: private boolean useParameters = false;
165: private boolean _useParameters = false;
166:
167: /** Should we make the cookies available in the stylesheet? (default is off) */
168: private boolean useCookies = false;
169: private boolean _useCookies = false;
170:
171: /** Should we info about the session available in the stylesheet? (default is off) */
172: private boolean useSessionInfo = false;
173: private boolean _useSessionInfo = false;
174:
175: /** Do we check included stylesheets for changes? */
176: private boolean checkIncludes = true;
177:
178: /** The trax TransformerHandler */
179: protected TransformerHandler transformerHandler;
180:
181: /** The validity of the Transformer */
182: protected SourceValidity transformerValidity;
183:
184: /** The Source */
185: private Source inputSource;
186: /** The parameters */
187: private Parameters par;
188: /** The source resolver */
189: private SourceResolver resolver;
190:
191: /** Default source, used to create specialized transformers by configuration */
192: private String defaultSrc;
193:
194: /** The XSLTProcessor */
195: private XSLTProcessor xsltProcessor;
196:
197: /** Did we finish the processing (is endDocument() called) */
198: private boolean finishedDocument = false;
199:
200: /** Xalan's DTMManager.getIncremental() method. See recycle() method to see what we need this for. */
201: private Method xalanDtmManagerGetIncrementalMethod;
202:
203: /** Exception that might occur during setConsumer */
204: private SAXException exceptionDuringSetConsumer;
205:
206: /** The error listener used by the stylesheet */
207: private TraxErrorListener errorListener;
208:
209: /**
210: * Configure this transformer.
211: */
212: public void configure(Configuration conf)
213: throws ConfigurationException {
214: Configuration child;
215:
216: child = conf.getChild("use-request-parameters");
217: this .useParameters = child.getValueAsBoolean(false);
218: this ._useParameters = this .useParameters;
219:
220: child = conf.getChild("use-cookies");
221: this .useCookies = child.getValueAsBoolean(false);
222: this ._useCookies = this .useCookies;
223:
224: child = conf.getChild("use-session-info");
225: this .useSessionInfo = child.getValueAsBoolean(false);
226: this ._useSessionInfo = this .useSessionInfo;
227:
228: child = conf.getChild("transformer-factory");
229: // traxFactory is null, if transformer-factory config is unspecified
230: final String traxFactory = child.getValue(null);
231:
232: child = conf.getChild("xslt-processor-role");
233: String xsltProcessorRole = child.getValue(XSLTProcessor.ROLE);
234: if (!xsltProcessorRole.startsWith(XSLTProcessor.ROLE)) {
235: xsltProcessorRole = XSLTProcessor.ROLE + '/'
236: + xsltProcessorRole;
237: }
238:
239: child = conf.getChild("check-includes");
240: this .checkIncludes = child
241: .getValueAsBoolean(this .checkIncludes);
242:
243: child = conf.getChild("default-src", false);
244: if (child != null) {
245: this .defaultSrc = child.getValue();
246: }
247:
248: if (getLogger().isDebugEnabled()) {
249: getLogger()
250: .debug("Use parameters is " + this .useParameters);
251: getLogger().debug("Use cookies is " + this .useCookies);
252: getLogger().debug(
253: "Use session info is " + this .useSessionInfo);
254: getLogger()
255: .debug("Use TrAX Processor " + xsltProcessorRole);
256: getLogger().debug(
257: "Check for included stylesheets is "
258: + this .checkIncludes);
259: if (traxFactory != null) {
260: getLogger().debug(
261: "Use TrAX Transformer Factory " + traxFactory);
262: } else {
263: getLogger().debug(
264: "Use default TrAX Transformer Factory.");
265: }
266: getLogger().debug("Default source = " + this .defaultSrc);
267: }
268:
269: try {
270: this .xsltProcessor = (XSLTProcessor) this .manager
271: .lookup(xsltProcessorRole);
272: if (traxFactory != null) {
273: this .xsltProcessor.setTransformerFactory(traxFactory);
274: }
275: } catch (ServiceException e) {
276: throw new ConfigurationException(
277: "Cannot load XSLT processor", e);
278: }
279:
280: try {
281: // see the recyle() method to see what we need this for
282: Class dtmManagerClass = Class
283: .forName("org.apache.xml.dtm.DTMManager");
284: xalanDtmManagerGetIncrementalMethod = dtmManagerClass
285: .getMethod("getIncremental", null);
286: } catch (ClassNotFoundException e) {
287: // do nothing -- user does not use xalan, so we don't need the dtm manager
288: } catch (NoSuchMethodException e) {
289: throw new ConfigurationException(
290: "Was not able to get getIncremental method from Xalan's DTMManager.",
291: e);
292: }
293: }
294:
295: /**
296: * Set the current <code>ServiceManager</code> instance used by this
297: * <code>Serviceable</code>.
298: */
299: public void service(ServiceManager manager) throws ServiceException {
300: this .manager = manager;
301: }
302:
303: /**
304: * Set the <code>SourceResolver</code>, the <code>Map</code> with
305: * the object model, the source and sitemap
306: * <code>Parameters</code> used to process the request.
307: */
308: public void setup(SourceResolver resolver, Map objectModel,
309: String src, Parameters par) throws SAXException,
310: ProcessingException, IOException {
311:
312: if (src == null && defaultSrc != null) {
313: if (getLogger().isDebugEnabled()) {
314: getLogger().debug(
315: "src is null, using default source "
316: + defaultSrc);
317: }
318: src = defaultSrc;
319: }
320:
321: if (src == null) {
322: throw new ProcessingException(
323: "Stylesheet URI can't be null");
324: }
325:
326: this .par = par;
327: this .objectModel = objectModel;
328: this .resolver = resolver;
329: try {
330: this .inputSource = resolver.resolveURI(src);
331: } catch (SourceException se) {
332: throw SourceUtil.handle("Unable to resolve " + src, se);
333: }
334: _useParameters = par.getParameterAsBoolean(
335: "use-request-parameters", this .useParameters);
336: _useCookies = par.getParameterAsBoolean("use-cookies",
337: this .useCookies);
338: _useSessionInfo = par.getParameterAsBoolean("use-session-info",
339: this .useSessionInfo);
340: final boolean _checkIncludes = par.getParameterAsBoolean(
341: "check-includes", this .checkIncludes);
342:
343: if (getLogger().isDebugEnabled()) {
344: getLogger().debug(
345: "Using stylesheet: '" + this .inputSource.getURI()
346: + "' in " + this );
347: getLogger().debug(
348: "Use parameters is " + this ._useParameters);
349: getLogger().debug("Use cookies is " + this ._useCookies);
350: getLogger().debug(
351: "Use session info is " + this ._useSessionInfo);
352: getLogger().debug(
353: "Check for included stylesheets is "
354: + _checkIncludes);
355: }
356:
357: // Get a Transformer Handler if we check for includes
358: // If we don't check the handler is get during setConsumer()
359: try {
360: if (_checkIncludes) {
361: XSLTProcessor.TransformerHandlerAndValidity handlerAndValidity = this .xsltProcessor
362: .getTransformerHandlerAndValidity(
363: this .inputSource, null);
364: this .transformerHandler = handlerAndValidity
365: .getTransfomerHandler();
366: this .transformerValidity = handlerAndValidity
367: .getTransfomerValidity();
368: } else {
369: this .transformerValidity = this .inputSource
370: .getValidity();
371: }
372: } catch (XSLTProcessorException se) {
373: throw new ProcessingException(
374: "Unable to get transformer handler for "
375: + this .inputSource.getURI(), se);
376: }
377: }
378:
379: /**
380: * Generate the unique key.
381: * This key must be unique inside the space of this component.
382: *
383: * @return The generated key hashes the src
384: */
385: public Serializable getKey() {
386: Map map = getLogicSheetParameters();
387: if (map == null) {
388: return this .inputSource.getURI();
389: }
390:
391: StringBuffer sb = new StringBuffer();
392: sb.append(this .inputSource.getURI());
393: Set entries = map.entrySet();
394: for (Iterator i = entries.iterator(); i.hasNext();) {
395: sb.append(';');
396: Map.Entry entry = (Map.Entry) i.next();
397: sb.append(entry.getKey());
398: sb.append('=');
399: sb.append(entry.getValue());
400: }
401: return sb.toString();
402: }
403:
404: /**
405: * Generate the validity object.
406: *
407: * @return The generated validity object or <code>null</code> if the
408: * component is currently not cacheable.
409: */
410: public SourceValidity getValidity() {
411: //
412: // VG: Key is generated using parameter/value pairs,
413: // so this information does not need to be verified again
414: // (if parameter added/removed or value changed, key should
415: // change also), only stylesheet's validity is included.
416: //
417: return this .transformerValidity;
418: }
419:
420: /**
421: * Set the <code>XMLConsumer</code> that will receive XML data.
422: */
423: public void setConsumer(XMLConsumer consumer) {
424:
425: if (this .transformerHandler == null) {
426: try {
427: this .transformerHandler = this .xsltProcessor
428: .getTransformerHandler(this .inputSource);
429: } catch (XSLTProcessorException se) {
430: // the exception will be thrown during startDocument()
431: this .exceptionDuringSetConsumer = new SAXException(
432: "Unable to get transformer handler for "
433: + this .inputSource.getURI(), se);
434: return;
435: }
436: }
437: final Map map = getLogicSheetParameters();
438: if (map != null) {
439: final javax.xml.transform.Transformer transformer = this .transformerHandler
440: .getTransformer();
441: final Iterator iterator = map.entrySet().iterator();
442: while (iterator.hasNext()) {
443: final Map.Entry entry = (Entry) iterator.next();
444: transformer.setParameter((String) entry.getKey(), entry
445: .getValue());
446: }
447: }
448:
449: super .setContentHandler(this .transformerHandler);
450: super .setLexicalHandler(this .transformerHandler);
451:
452: if (this .transformerHandler instanceof LogEnabled) {
453: ((LogEnabled) this .transformerHandler)
454: .enableLogging(getLogger());
455: }
456: // According to TrAX specs, all TransformerHandlers are LexicalHandlers
457: final SAXResult result = new SAXResult(consumer);
458: result.setLexicalHandler(consumer);
459: this .transformerHandler.setResult(result);
460:
461: this .errorListener = new TraxErrorListener(getLogger(),
462: this .inputSource.getURI());
463: this .transformerHandler.getTransformer().setErrorListener(
464: this .errorListener);
465: }
466:
467: /**
468: * Get the parameters for the logicsheet
469: */
470: protected Map getLogicSheetParameters() {
471: if (this .logicSheetParameters != null) {
472: return this .logicSheetParameters;
473: }
474: HashMap map = null;
475: if (par != null) {
476: String[] params = par.getNames();
477: if (params != null) {
478: for (int i = 0; i < params.length; i++) {
479: String name = params[i];
480: if (isValidXSLTParameterName(name)) {
481: String value = par.getParameter(name, null);
482: if (value != null) {
483: if (map == null) {
484: map = new HashMap(params.length);
485: }
486: map.put(name, value);
487: }
488: }
489: }
490: }
491: }
492:
493: if (this ._useParameters) {
494: Request request = ObjectModelHelper.getRequest(objectModel);
495:
496: Enumeration parameters = request.getParameterNames();
497: if (parameters != null) {
498: while (parameters.hasMoreElements()) {
499: String name = (String) parameters.nextElement();
500: if (isValidXSLTParameterName(name)) {
501: String value = request.getParameter(name);
502: if (map == null) {
503: map = new HashMap();
504: }
505: map.put(name, value);
506: }
507: }
508: }
509: }
510:
511: if (this ._useSessionInfo) {
512: final Request request = ObjectModelHelper
513: .getRequest(objectModel);
514: if (map == null) {
515: map = new HashMap(6);
516: }
517:
518: final Session session = request.getSession(false);
519: if (session != null) {
520: map.put("session-available", "true");
521: map.put("session-is-new", BooleanUtils
522: .toStringTrueFalse(session.isNew()));
523: map.put("session-id-from-cookie", BooleanUtils
524: .toStringTrueFalse(request
525: .isRequestedSessionIdFromCookie()));
526: map.put("session-id-from-url", BooleanUtils
527: .toStringTrueFalse(request
528: .isRequestedSessionIdFromURL()));
529: map.put("session-valid", BooleanUtils
530: .toStringTrueFalse(request
531: .isRequestedSessionIdValid()));
532: map.put("session-id", session.getId());
533: } else {
534: map.put("session-available", "false");
535: }
536: }
537:
538: if (this ._useCookies) {
539: Request request = ObjectModelHelper.getRequest(objectModel);
540: Cookie cookies[] = request.getCookies();
541: if (cookies != null) {
542: for (int i = 0; i < cookies.length; i++) {
543: String name = cookies[i].getName();
544: if (isValidXSLTParameterName(name)) {
545: String value = cookies[i].getValue();
546: if (map == null) {
547: map = new HashMap(cookies.length);
548: }
549: map.put(name, value);
550: }
551: }
552: }
553: }
554: this .logicSheetParameters = map;
555: return this .logicSheetParameters;
556: }
557:
558: /**
559: * Test if the name is a valid parameter name for XSLT
560: */
561: static boolean isValidXSLTParameterName(String name) {
562: if (name.length() == 0) {
563: return false;
564: }
565:
566: char c = name.charAt(0);
567: if (!(Character.isLetter(c) || c == '_')) {
568: return false;
569: }
570:
571: for (int i = name.length() - 1; i > 1; i--) {
572: c = name.charAt(i);
573: if (!(Character.isLetterOrDigit(c) || c == '-' || c == '_' || c == '.')) {
574: return false;
575: }
576: }
577: return true;
578: }
579:
580: /**
581: * Disposable
582: */
583: public void dispose() {
584: if (this .manager != null) {
585: this .manager.release(this .xsltProcessor);
586: this .xsltProcessor = null;
587: this .manager = null;
588: }
589: }
590:
591: /**
592: * Recyclable
593: */
594: public void recycle() {
595: this .objectModel = null;
596: if (this .inputSource != null) {
597: this .resolver.release(this .inputSource);
598: this .inputSource = null;
599: }
600: this .resolver = null;
601: this .par = null;
602: if (!this .finishedDocument && transformerHandler != null) {
603: // This situation will only occur if an exception occured during pipeline execution.
604: // If Xalan is used in incremental mode, it is important that endDocument is called, otherwise
605: // the thread on which it runs the transformation will keep waiting.
606: // However, calling endDocument will cause the pipeline to continue executing, and thus the
607: // serializer will write output to the outputstream after what's already there (the error page),
608: // see also bug 13186.
609: if (xalanDtmManagerGetIncrementalMethod != null
610: && transformerHandler
611: .getClass()
612: .getName()
613: .equals(
614: "org.apache.xalan.transformer.TransformerHandlerImpl")) {
615: try {
616: final boolean incremental = ((Boolean) xalanDtmManagerGetIncrementalMethod
617: .invoke(null, null)).booleanValue();
618: if (incremental) {
619: super .endDocument();
620: }
621: } catch (Exception ignore) {
622: }
623: }
624: }
625: this .finishedDocument = true;
626: this .logicSheetParameters = null;
627: this .transformerHandler = null;
628: this .transformerValidity = null;
629: this .exceptionDuringSetConsumer = null;
630: this .errorListener = null;
631: super .recycle();
632: }
633:
634: /**
635: * Fix for stopping hanging threads of Xalan
636: */
637: public void endDocument() throws SAXException {
638: try {
639: super .endDocument();
640: } catch (Exception e) {
641:
642: Throwable realEx = this .errorListener.getThrowable();
643: if (realEx == null)
644: realEx = e;
645:
646: if (realEx instanceof RuntimeException) {
647: throw (RuntimeException) realEx;
648: }
649:
650: if (realEx instanceof SAXException) {
651: throw (SAXException) realEx;
652: }
653:
654: if (realEx instanceof Error) {
655: throw (Error) realEx;
656: }
657:
658: throw new NestableRuntimeException(realEx);
659: }
660: this .finishedDocument = true;
661: }
662:
663: /* (non-Javadoc)
664: * @see org.xml.sax.ContentHandler#startDocument()
665: */
666: public void startDocument() throws SAXException {
667: // did an exception occur during setConsumer?
668: // if so, throw it here
669: if (this .exceptionDuringSetConsumer != null) {
670: throw this .exceptionDuringSetConsumer;
671: }
672: this .finishedDocument = false;
673: super.startDocument();
674: }
675: }
|