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: package org.apache.cocoon.transformation;
018:
019: import java.io.BufferedReader;
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.io.InputStreamReader;
023: import java.io.Reader;
024: import java.io.Serializable;
025: import java.net.MalformedURLException;
026: import java.util.Map;
027:
028: import org.apache.avalon.framework.CascadingException;
029: import org.apache.avalon.framework.CascadingRuntimeException;
030: import org.apache.avalon.framework.parameters.Parameters;
031: import org.apache.avalon.framework.service.ServiceManager;
032: import org.apache.avalon.framework.service.Serviceable;
033: import org.apache.cocoon.ProcessingException;
034: import org.apache.cocoon.ResourceNotFoundException;
035: import org.apache.cocoon.caching.CacheableProcessingComponent;
036: import org.apache.cocoon.components.source.SourceUtil;
037: import org.apache.cocoon.components.source.impl.MultiSourceValidity;
038: import org.apache.cocoon.components.xpointer.XPointer;
039: import org.apache.cocoon.components.xpointer.XPointerContext;
040: import org.apache.cocoon.components.xpointer.parser.ParseException;
041: import org.apache.cocoon.components.xpointer.parser.XPointerFrameworkParser;
042: import org.apache.cocoon.environment.SourceResolver;
043: import org.apache.cocoon.util.NetUtils;
044: import org.apache.cocoon.xml.AbstractXMLPipe;
045: import org.apache.cocoon.xml.IncludeXMLConsumer;
046: import org.apache.cocoon.xml.XMLBaseSupport;
047: import org.apache.cocoon.xml.XMLConsumer;
048: import org.apache.excalibur.source.Source;
049: import org.apache.excalibur.source.SourceException;
050: import org.apache.excalibur.source.SourceNotFoundException;
051: import org.apache.excalibur.source.SourceValidity;
052: import org.xml.sax.Attributes;
053: import org.xml.sax.ContentHandler;
054: import org.xml.sax.Locator;
055: import org.xml.sax.SAXException;
056: import org.xml.sax.ext.LexicalHandler;
057:
058: /**
059: * @cocoon.sitemap.component.documentation
060: * Implementation of an XInclude transformer.
061: *
062: * @cocoon.sitemap.component.name xinclude
063: * @cocoon.sitemap.component.logger sitemap.transformer.xinclude
064: *
065: * @cocoon.sitemap.component.pooling.max 16
066: *
067: * Implementation of an XInclude transformer. It supports xml:base attributes,
068: * XPointer fragment identifiers (see the xpointer package to see what exactly is
069: * supported), fallback elements, and does xinclude processing on the included content
070: * and on the content of fallback elements (with loop inclusion detection).
071: *
072: * @author <a href="mailto:balld@webslingerZ.com">Donald Ball</a> (wrote the original version)
073: * @version SVN $Id: XIncludeTransformer.java 433543 2006-08-22 06:22:54Z crossley $
074: */
075: public class XIncludeTransformer extends AbstractTransformer implements
076: Serviceable, CacheableProcessingComponent {
077: protected SourceResolver resolver;
078: protected ServiceManager manager;
079: private XIncludePipe xIncludePipe;
080:
081: /**
082: * @deprecated Should be removed in cocoon 2.2. Use javax.xml.XMLConstants.XML_NS_URI instead.
083: */
084: public static final String XMLBASE_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
085: public static final String XMLBASE_ATTRIBUTE = "base";
086:
087: public static final String XINCLUDE_NAMESPACE_URI = "http://www.w3.org/2001/XInclude";
088: public static final String XINCLUDE_INCLUDE_ELEMENT = "include";
089: public static final String XINCLUDE_FALLBACK_ELEMENT = "fallback";
090: public static final String XINCLUDE_INCLUDE_ELEMENT_HREF_ATTRIBUTE = "href";
091: public static final String XINCLUDE_INCLUDE_ELEMENT_XPOINTER_ATTRIBUTE = "xpointer";
092: public static final String XINCLUDE_INCLUDE_ELEMENT_PARSE_ATTRIBUTE = "parse";
093:
094: private static final String XINCLUDE_CACHE_KEY = "XInclude";
095:
096: /** The {@link SourceValidity} instance associated with this request. */
097: protected MultiSourceValidity validity;
098:
099: public void setup(SourceResolver resolver, Map objectModel,
100: String source, Parameters parameters)
101: throws ProcessingException, SAXException, IOException {
102: this .resolver = resolver;
103: this .validity = new MultiSourceValidity(resolver,
104: MultiSourceValidity.CHECK_ALWAYS);
105: this .xIncludePipe = new XIncludePipe();
106: this .xIncludePipe.enableLogging(getLogger());
107: this .xIncludePipe.init(null, null);
108: super .setContentHandler(xIncludePipe);
109: super .setLexicalHandler(xIncludePipe);
110: }
111:
112: public void setConsumer(XMLConsumer consumer) {
113: xIncludePipe.setConsumer(consumer);
114: }
115:
116: public void setContentHandler(ContentHandler handler) {
117: xIncludePipe.setContentHandler(handler);
118: }
119:
120: public void setLexicalHandler(LexicalHandler handler) {
121: xIncludePipe.setLexicalHandler(handler);
122: }
123:
124: public void service(ServiceManager manager) {
125: this .manager = manager;
126: }
127:
128: /** Key to be used for caching */
129: public Serializable getKey() {
130: return XINCLUDE_CACHE_KEY;
131: }
132:
133: /** Get the validity for this transform */
134: public SourceValidity getValidity() {
135: return this .validity;
136: }
137:
138: public void recycle() {
139: // Reset all variables to initial state.
140: this .resolver = null;
141: this .validity = null;
142: this .xIncludePipe = null;
143: super .recycle();
144: }
145:
146: /**
147: * XMLPipe that processes XInclude elements. To perform XInclude processing on included content,
148: * this class is instantiated recursively.
149: */
150: private class XIncludePipe extends AbstractXMLPipe {
151: /** Helper class to keep track of xml:base attributes */
152: private XMLBaseSupport xmlBaseSupport;
153: /** The nesting level of xi:include elements that have been encountered. */
154: private int xIncludeElementLevel = 0;
155:
156: /** The nesting level of fallback that should be used */
157: private int useFallbackLevel = 0;
158:
159: /** The nesting level of xi:fallback elements that have been encountered. */
160: private int fallbackElementLevel;
161:
162: /**
163: * In case {@link #useFallbackLevel} > 0, then this should contain the
164: * exception that caused fallback to be needed. In the case of nested
165: * include elements it will contain only the deepest exception.
166: */
167: private Exception fallBackException;
168:
169: /**
170: * Locator of the current stream, stored here so that it can be restored after
171: * another document send its content to the consumer.
172: */
173: private Locator locator;
174:
175: /**
176: * Value of the href attribute of the xi:include element that caused the creation of the this
177: * XIncludePipe. Used to detect loop inclusions.
178: */
179: private String href;
180:
181: /**
182: * Value of the xpointer attribute of the xi:include element that caused the creation of this
183: * XIncludePipe. Used to detect loop inclusions.
184: */
185: private String xpointer;
186:
187: private XIncludePipe parent;
188:
189: public void init(String uri, String xpointer) {
190: this .href = uri;
191: this .xpointer = xpointer;
192: this .xmlBaseSupport = new XMLBaseSupport(resolver,
193: getLogger());
194: }
195:
196: public void setParent(XIncludePipe parent) {
197: this .parent = parent;
198: }
199:
200: public XIncludePipe getParent() {
201: return parent;
202: }
203:
204: public String getHref() {
205: return href;
206: }
207:
208: public String getXpointer() {
209: return xpointer;
210: }
211:
212: /**
213: * Determine whether the pipe is currently in a state where contents
214: * should be evaluated, i.e. xi:include elements should be resolved
215: * and elements in other namespaces should be copied through. Will
216: * return false for fallback contents within a successful xi:include,
217: * and true for contents outside any xi:include or within an xi:fallback
218: * for an unsuccessful xi:include.
219: */
220: private boolean isEvaluatingContent() {
221: return xIncludeElementLevel == 0
222: || (fallbackElementLevel > 0 && fallbackElementLevel == useFallbackLevel);
223: }
224:
225: public void endDocument() throws SAXException {
226: // We won't be getting any more sources so mark the MultiSourceValidity as finished.
227: validity.close();
228: super .endDocument();
229: }
230:
231: public void startElement(String uri, String name, String raw,
232: Attributes attr) throws SAXException {
233: // Track xml:base context:
234: xmlBaseSupport.startElement(uri, name, raw, attr);
235: // Handle elements in xinclude namespace:
236: if (XINCLUDE_NAMESPACE_URI.equals(uri)) {
237: // Handle xi:include:
238: if (XINCLUDE_INCLUDE_ELEMENT.equals(name)) {
239: // Process the include, unless in an ignored fallback:
240: if (isEvaluatingContent()) {
241: String href = attr
242: .getValue("",
243: XINCLUDE_INCLUDE_ELEMENT_HREF_ATTRIBUTE);
244: String parse = attr
245: .getValue("",
246: XINCLUDE_INCLUDE_ELEMENT_PARSE_ATTRIBUTE);
247: String xpointer = attr
248: .getValue("",
249: XINCLUDE_INCLUDE_ELEMENT_XPOINTER_ATTRIBUTE);
250:
251: try {
252: processXIncludeElement(href, parse,
253: xpointer);
254: } catch (ProcessingException e) {
255: getLogger()
256: .debug("Rethrowing exception", e);
257: throw new SAXException(e);
258: } catch (IOException e) {
259: getLogger()
260: .debug("Rethrowing exception", e);
261: throw new SAXException(e);
262: }
263: }
264: xIncludeElementLevel++;
265: } else if (XINCLUDE_FALLBACK_ELEMENT.equals(name)) {
266: // Handle xi:fallback
267: fallbackElementLevel++;
268: } else {
269: // Unknown element:
270: throw new SAXException("Unknown XInclude element "
271: + raw + " at " + getLocation());
272: }
273: } else if (isEvaluatingContent()) {
274: // Copy other elements through when appropriate:
275: super .startElement(uri, name, raw, attr);
276: }
277: }
278:
279: public void endElement(String uri, String name, String raw)
280: throws SAXException {
281: // Track xml:base context:
282: xmlBaseSupport.endElement(uri, name, raw);
283:
284: // Handle elements in xinclude namespace:
285: if (XINCLUDE_NAMESPACE_URI.equals(uri)) {
286: // Handle xi:include:
287: if (XINCLUDE_INCLUDE_ELEMENT.equals(name)) {
288: xIncludeElementLevel--;
289: if (useFallbackLevel > xIncludeElementLevel) {
290: useFallbackLevel = xIncludeElementLevel;
291: }
292: } else if (XINCLUDE_FALLBACK_ELEMENT.equals(name)) {
293: // Handle xi:fallback:
294: fallbackElementLevel--;
295: }
296: } else if (isEvaluatingContent()) {
297: // Copy other elements through when appropriate:
298: super .endElement(uri, name, raw);
299: }
300: }
301:
302: public void startPrefixMapping(String prefix, String uri)
303: throws SAXException {
304: if (isEvaluatingContent()) {
305: super .startPrefixMapping(prefix, uri);
306: }
307: }
308:
309: public void endPrefixMapping(String prefix) throws SAXException {
310: if (isEvaluatingContent()) {
311: super .endPrefixMapping(prefix);
312: }
313: }
314:
315: public void characters(char c[], int start, int len)
316: throws SAXException {
317: if (isEvaluatingContent()) {
318: super .characters(c, start, len);
319: }
320: }
321:
322: public void ignorableWhitespace(char c[], int start, int len)
323: throws SAXException {
324: if (isEvaluatingContent()) {
325: super .ignorableWhitespace(c, start, len);
326: }
327: }
328:
329: public void processingInstruction(String target, String data)
330: throws SAXException {
331: if (isEvaluatingContent()) {
332: super .processingInstruction(target, data);
333: }
334: }
335:
336: public void skippedEntity(String name) throws SAXException {
337: if (isEvaluatingContent()) {
338: super .skippedEntity(name);
339: }
340: }
341:
342: public void startEntity(String name) throws SAXException {
343: if (isEvaluatingContent()) {
344: super .startEntity(name);
345: }
346: }
347:
348: public void endEntity(String name) throws SAXException {
349: if (isEvaluatingContent()) {
350: super .endEntity(name);
351: }
352: }
353:
354: public void startCDATA() throws SAXException {
355: if (isEvaluatingContent()) {
356: super .startCDATA();
357: }
358: }
359:
360: public void endCDATA() throws SAXException {
361: if (isEvaluatingContent()) {
362: super .endCDATA();
363: }
364: }
365:
366: public void comment(char ch[], int start, int len)
367: throws SAXException {
368: if (isEvaluatingContent()) {
369: super .comment(ch, start, len);
370: }
371: }
372:
373: public void setDocumentLocator(Locator locator) {
374: try {
375: if (getLogger().isDebugEnabled()) {
376: getLogger().debug(
377: "setDocumentLocator called "
378: + locator.getSystemId());
379: }
380:
381: // When using SAXON to serialize a DOM tree to SAX, a locator is passed with a "null" system id
382: if (locator.getSystemId() != null) {
383: Source source = resolver.resolveURI(locator
384: .getSystemId());
385: try {
386: xmlBaseSupport.setDocumentLocation(source
387: .getURI());
388: // only for the "root" XIncludePipe, we'll have to set the href here, in the other cases
389: // the href is taken from the xi:include href attribute
390: if (href == null)
391: href = source.getURI();
392: } finally {
393: resolver.release(source);
394: }
395: }
396: } catch (Exception e) {
397: throw new CascadingRuntimeException(
398: "Error in XIncludeTransformer while trying to resolve base URL for document",
399: e);
400: }
401: this .locator = locator;
402: super .setDocumentLocator(locator);
403: }
404:
405: protected void processXIncludeElement(String href,
406: String parse, String xpointer) throws SAXException,
407: ProcessingException, IOException {
408: if (getLogger().isDebugEnabled()) {
409: getLogger().debug(
410: "Processing XInclude element: href=" + href
411: + ", parse=" + parse + ", xpointer="
412: + xpointer);
413: }
414:
415: // Default for @parse is "xml"
416: if (parse == null) {
417: parse = "xml";
418: }
419: Source url = null;
420:
421: try {
422: int fragmentIdentifierPos = href.indexOf('#');
423: if (fragmentIdentifierPos != -1) {
424: getLogger()
425: .warn(
426: "Fragment identifer found in 'href' attribute: "
427: + href
428: + "\nFragment identifiers are forbidden by the XInclude specification. "
429: + "They are still handled by XIncludeTransformer for backward "
430: + "compatibility, but their use is deprecated and will be prohibited "
431: + "in a future release. Use the 'xpointer' attribute instead.");
432: if (xpointer == null) {
433: xpointer = href
434: .substring(fragmentIdentifierPos + 1);
435: }
436: href = href.substring(0, fragmentIdentifierPos);
437: }
438:
439: // An empty or absent href is a reference to the current document -- this can be different than the current base
440: if (href == null || href.length() == 0) {
441: if (this .href == null) {
442: throw new SAXException(
443: "XIncludeTransformer: encountered empty href (= href pointing to the current document) but the location of the current document is unknown.");
444: }
445: // The following can be simplified once fragment identifiers are prohibited
446: int fragmentIdentifierPos2 = this .href.indexOf('#');
447: if (fragmentIdentifierPos2 != -1)
448: href = this .href.substring(0,
449: fragmentIdentifierPos2);
450: else
451: href = this .href;
452: }
453:
454: url = xmlBaseSupport.makeAbsolute(href);
455: if (getLogger().isDebugEnabled()) {
456: getLogger().debug(
457: "URL: " + url.getURI() + "\nXPointer: "
458: + xpointer);
459: }
460:
461: // add the source to the SourceValidity
462: validity.addSource(url);
463:
464: if (parse.equals("text")) {
465: getLogger().debug("Parse type is text");
466: if (xpointer != null) {
467: throw new SAXException(
468: "xpointer attribute must not be present when parse='text': "
469: + getLocation());
470: }
471: InputStream is = null;
472: InputStreamReader isr = null;
473: Reader reader = null;
474: try {
475: is = url.getInputStream();
476: isr = new InputStreamReader(is);
477: reader = new BufferedReader(isr);
478: int read;
479: char ary[] = new char[1024 * 4];
480: while ((read = reader.read(ary)) != -1) {
481: super .characters(ary, 0, read);
482: }
483: } catch (SourceNotFoundException e) {
484: useFallbackLevel++;
485: fallBackException = new CascadingException(
486: "Resource not found: " + url.getURI());
487: getLogger().error(
488: "xIncluded resource not found: "
489: + url.getURI(), e);
490: } finally {
491: if (reader != null)
492: reader.close();
493: if (isr != null)
494: isr.close();
495: if (is != null)
496: is.close();
497: }
498: } else if (parse.equals("xml")) {
499: getLogger().debug("Parse type is XML");
500:
501: // Check loop inclusion
502: if (isLoopInclusion(url.getURI(), xpointer)) {
503: throw new ProcessingException(
504: "Detected loop inclusion of href="
505: + url.getURI() + ", xpointer="
506: + xpointer);
507: }
508:
509: XIncludePipe subPipe = new XIncludePipe();
510: subPipe.enableLogging(getLogger());
511: subPipe.init(url.getURI(), xpointer);
512: subPipe.setConsumer(xmlConsumer);
513: subPipe.setParent(this );
514:
515: try {
516: if (xpointer != null && xpointer.length() > 0) {
517: XPointer xptr;
518: xptr = XPointerFrameworkParser
519: .parse(NetUtils
520: .decodePath(xpointer));
521: XPointerContext context = new XPointerContext(
522: xpointer, url, subPipe,
523: getLogger(), manager);
524: xptr.process(context);
525: } else {
526: SourceUtil.toSAX(url,
527: new IncludeXMLConsumer(subPipe));
528: }
529: // restore locator on the consumer
530: if (locator != null)
531: xmlConsumer.setDocumentLocator(locator);
532: } catch (ResourceNotFoundException e) {
533: useFallbackLevel++;
534: fallBackException = new CascadingException(
535: "Resource not found: " + url.getURI());
536: getLogger().error(
537: "xIncluded resource not found: "
538: + url.getURI(), e);
539: } catch (ParseException e) {
540: // this exception is thrown in case of an invalid xpointer expression
541: useFallbackLevel++;
542: fallBackException = new CascadingException(
543: "Error parsing xPointer expression", e);
544: fallBackException.fillInStackTrace();
545: getLogger()
546: .error(
547: "Error parsing XPointer expression, will try to use fallback.",
548: e);
549: } catch (SAXException e) {
550: getLogger().error(
551: "Error in processXIncludeElement", e);
552: throw e;
553: } catch (ProcessingException e) {
554: getLogger().error(
555: "Error in processXIncludeElement", e);
556: throw e;
557: } catch (MalformedURLException e) {
558: useFallbackLevel++;
559: fallBackException = e;
560: getLogger()
561: .error(
562: "Error processing an xInclude, will try to use fallback.",
563: e);
564: } catch (IOException e) {
565: useFallbackLevel++;
566: fallBackException = e;
567: getLogger()
568: .error(
569: "Error processing an xInclude, will try to use fallback.",
570: e);
571: }
572: } else {
573: throw new SAXException(
574: "Found 'parse' attribute with unknown value "
575: + parse + " at " + getLocation());
576: }
577: } catch (SourceException se) {
578: throw SourceUtil.handle(se);
579: } finally {
580: if (url != null) {
581: resolver.release(url);
582: }
583: }
584: }
585:
586: public boolean isLoopInclusion(String uri, String xpointer) {
587: if (xpointer == null) {
588: xpointer = "";
589: }
590:
591: if (uri.equals(this .href)
592: && xpointer.equals(this .xpointer == null ? ""
593: : this .xpointer)) {
594: return true;
595: }
596:
597: XIncludePipe parent = getParent();
598: while (parent != null) {
599: if (uri.equals(parent.getHref())
600: && xpointer
601: .equals(parent.getXpointer() == null ? ""
602: : parent.getXpointer())) {
603: return true;
604: }
605: parent = parent.getParent();
606: }
607: return false;
608: }
609:
610: private String getLocation() {
611: if (this .locator == null) {
612: return "unknown location";
613: } else {
614: return this .locator.getSystemId() + ":"
615: + this .locator.getColumnNumber() + ":"
616: + this.locator.getLineNumber();
617: }
618: }
619: }
620: }
|