| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.cocoon.transformation; |
| |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.io.Serializable; |
| import java.net.MalformedURLException; |
| import java.util.Map; |
| |
| import org.apache.avalon.framework.CascadingException; |
| import org.apache.avalon.framework.CascadingRuntimeException; |
| import org.apache.avalon.framework.parameters.Parameters; |
| import org.apache.avalon.framework.service.ServiceManager; |
| import org.apache.avalon.framework.service.Serviceable; |
| import org.apache.cocoon.ProcessingException; |
| import org.apache.cocoon.ResourceNotFoundException; |
| import org.apache.cocoon.caching.CacheableProcessingComponent; |
| import org.apache.cocoon.components.source.SourceUtil; |
| import org.apache.cocoon.components.source.impl.MultiSourceValidity; |
| import org.apache.cocoon.components.xpointer.XPointer; |
| import org.apache.cocoon.components.xpointer.XPointerContext; |
| import org.apache.cocoon.components.xpointer.parser.ParseException; |
| import org.apache.cocoon.components.xpointer.parser.XPointerFrameworkParser; |
| import org.apache.cocoon.environment.SourceResolver; |
| import org.apache.cocoon.util.NetUtils; |
| import org.apache.cocoon.xml.AbstractXMLPipe; |
| import org.apache.cocoon.xml.IncludeXMLConsumer; |
| import org.apache.cocoon.xml.XMLBaseSupport; |
| import org.apache.cocoon.xml.XMLConsumer; |
| import org.apache.excalibur.source.Source; |
| import org.apache.excalibur.source.SourceException; |
| import org.apache.excalibur.source.SourceNotFoundException; |
| import org.apache.excalibur.source.SourceValidity; |
| import org.xml.sax.Attributes; |
| import org.xml.sax.ContentHandler; |
| import org.xml.sax.Locator; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.ext.LexicalHandler; |
| import org.xml.sax.helpers.AttributesImpl; |
| |
| /** |
| * @cocoon.sitemap.component.documentation |
| * Implementation of an XInclude transformer. |
| * |
| * @cocoon.sitemap.component.name xinclude |
| * @cocoon.sitemap.component.logger sitemap.transformer.xinclude |
| * |
| * @cocoon.sitemap.component.pooling.max 16 |
| * |
| * Implementation of an XInclude transformer. It supports xml:base attributes, |
| * XPointer fragment identifiers (see the xpointer package to see what exactly is |
| * supported), fallback elements, and does xinclude processing on the included content |
| * and on the content of fallback elements (with loop inclusion detection). |
| * |
| * @author <a href="mailto:balld@webslingerZ.com">Donald Ball</a> (wrote the original version) |
| * @version SVN $Id$ |
| */ |
| public class XIncludeTransformer extends AbstractTransformer implements Serviceable, CacheableProcessingComponent { |
| protected SourceResolver resolver; |
| protected ServiceManager manager; |
| private XIncludePipe xIncludePipe; |
| |
| /** |
| * @deprecated Should be removed in cocoon 2.2. Use javax.xml.XMLConstants.XML_NS_URI instead. |
| */ |
| public static final String XMLBASE_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace"; |
| public static final String XMLBASE_NAMESPACE_PREFIX = "xml"; |
| public static final String XMLBASE_ATTRIBUTE = "base"; |
| public static final String XMLBASE_ATTRIBUTE_TYPE = "CDATA"; |
| |
| public static final String XINCLUDE_NAMESPACE_URI = "http://www.w3.org/2001/XInclude"; |
| public static final String XINCLUDE_INCLUDE_ELEMENT = "include"; |
| public static final String XINCLUDE_FALLBACK_ELEMENT = "fallback"; |
| public static final String XINCLUDE_INCLUDE_ELEMENT_HREF_ATTRIBUTE = "href"; |
| public static final String XINCLUDE_INCLUDE_ELEMENT_XPOINTER_ATTRIBUTE = "xpointer"; |
| public static final String XINCLUDE_INCLUDE_ELEMENT_PARSE_ATTRIBUTE = "parse"; |
| |
| private static final String XINCLUDE_CACHE_KEY = "XInclude"; |
| |
| /** The {@link SourceValidity} instance associated with this request. */ |
| protected MultiSourceValidity validity; |
| |
| public void setup(SourceResolver resolver, Map objectModel, String source, Parameters parameters) |
| throws ProcessingException, SAXException, IOException { |
| this.resolver = resolver; |
| this.validity = new MultiSourceValidity(resolver, MultiSourceValidity.CHECK_ALWAYS); |
| this.xIncludePipe = new XIncludePipe(); |
| this.xIncludePipe.enableLogging(getLogger()); |
| this.xIncludePipe.init(null, null); |
| super.setContentHandler(xIncludePipe); |
| super.setLexicalHandler(xIncludePipe); |
| } |
| |
| public void setConsumer(XMLConsumer consumer) { |
| xIncludePipe.setConsumer(consumer); |
| } |
| |
| public void setContentHandler(ContentHandler handler) { |
| xIncludePipe.setContentHandler(handler); |
| } |
| |
| public void setLexicalHandler(LexicalHandler handler) { |
| xIncludePipe.setLexicalHandler(handler); |
| } |
| |
| public void service(ServiceManager manager) { |
| this.manager = manager; |
| } |
| |
| /** Key to be used for caching */ |
| public Serializable getKey() { |
| return XINCLUDE_CACHE_KEY; |
| } |
| |
| /** Get the validity for this transform */ |
| public SourceValidity getValidity() { |
| return this.validity; |
| } |
| |
| public void recycle() |
| { |
| // Reset all variables to initial state. |
| this.resolver = null; |
| this.validity = null; |
| this.xIncludePipe = null; |
| super.recycle(); |
| } |
| |
| /** |
| * XMLPipe that processes XInclude elements. To perform XInclude processing on included content, |
| * this class is instantiated recursively. |
| */ |
| private class XIncludePipe extends AbstractXMLPipe { |
| /** Helper class to keep track of xml:base attributes */ |
| private XMLBaseSupport xmlBaseSupport; |
| /** The nesting level of xi:include elements that have been encountered. */ |
| private int xIncludeElementLevel = 0; |
| |
| /** The nesting level of fallback that should be used */ |
| private int useFallbackLevel = 0; |
| |
| /** The nesting level of xi:fallback elements that have been encountered. */ |
| private int fallbackElementLevel; |
| |
| /** |
| * In case {@link #useFallbackLevel} > 0, then this should contain the |
| * exception that caused fallback to be needed. In the case of nested |
| * include elements it will contain only the deepest exception. |
| */ |
| private Exception fallBackException; |
| |
| /** |
| * Locator of the current stream, stored here so that it can be restored after |
| * another document send its content to the consumer. |
| */ |
| private Locator locator; |
| |
| /** |
| * Value of the href attribute of the xi:include element that caused the creation of the this |
| * XIncludePipe. Used to detect loop inclusions. |
| */ |
| private String href; |
| |
| /** |
| * Value of the xpointer attribute of the xi:include element that caused the creation of this |
| * XIncludePipe. Used to detect loop inclusions. |
| */ |
| private String xpointer; |
| |
| /** |
| * Value of the current element level. Used to determine when to insert |
| * xml:base attributes for base URI fixup. |
| */ |
| private int level = 0; |
| |
| /** |
| * Base URI of the parent of the current element. Used to determine |
| * if base URI fixup is necessary. |
| */ |
| private String parentBaseURI = null; |
| |
| private XIncludePipe parent; |
| |
| public void init(String uri, String xpointer) { |
| this.href = uri; |
| this.xpointer = xpointer; |
| this.xmlBaseSupport = new XMLBaseSupport(resolver, getLogger()); |
| } |
| |
| public void setParent(XIncludePipe parent) { |
| this.parent = parent; |
| } |
| |
| public XIncludePipe getParent() { |
| return parent; |
| } |
| |
| public String getHref() { |
| return href; |
| } |
| |
| public String getXpointer() { |
| return xpointer; |
| } |
| |
| /** |
| * Determine whether the pipe is currently in a state where contents |
| * should be evaluated, i.e. xi:include elements should be resolved |
| * and elements in other namespaces should be copied through. Will |
| * return false for fallback contents within a successful xi:include, |
| * and true for contents outside any xi:include or within an xi:fallback |
| * for an unsuccessful xi:include. |
| */ |
| private boolean isEvaluatingContent() { |
| return xIncludeElementLevel == 0 || (fallbackElementLevel > 0 && fallbackElementLevel == useFallbackLevel); |
| } |
| |
| public void endDocument() throws SAXException { |
| // We won't be getting any more sources so mark the MultiSourceValidity as finished. |
| validity.close(); |
| super.endDocument(); |
| } |
| |
| public void startElement(String uri, String name, String raw, Attributes attr) throws SAXException { |
| // Track xml:base context: |
| parentBaseURI = xmlBaseSupport.getCurrentBase(); |
| xmlBaseSupport.startElement(uri, name, raw, attr); |
| this.level++; |
| // Handle elements in xinclude namespace: |
| if (XINCLUDE_NAMESPACE_URI.equals(uri)) { |
| // Handle xi:include: |
| if (XINCLUDE_INCLUDE_ELEMENT.equals(name)) { |
| // Process the include, unless in an ignored fallback: |
| if (isEvaluatingContent()) { |
| String href = attr.getValue("", XINCLUDE_INCLUDE_ELEMENT_HREF_ATTRIBUTE); |
| String parse = attr.getValue("", XINCLUDE_INCLUDE_ELEMENT_PARSE_ATTRIBUTE); |
| String xpointer = attr.getValue("", XINCLUDE_INCLUDE_ELEMENT_XPOINTER_ATTRIBUTE); |
| |
| try { |
| processXIncludeElement(href, parse, xpointer); |
| } catch (ProcessingException e) { |
| getLogger().debug("Rethrowing exception", e); |
| throw new SAXException(e); |
| } catch (IOException e) { |
| getLogger().debug("Rethrowing exception", e); |
| throw new SAXException(e); |
| } |
| } |
| xIncludeElementLevel++; |
| } else if (XINCLUDE_FALLBACK_ELEMENT.equals(name)) { |
| // Handle xi:fallback |
| fallbackElementLevel++; |
| } else { |
| // Unknown element: |
| throw new SAXException("Unknown XInclude element " + raw + " at " + getLocation()); |
| } |
| } else if (isEvaluatingContent()) { |
| // Copy other elements through when appropriate, |
| // performing base URI fixup when necessary. |
| if(mustAddBaseAttr()) |
| super.startElement(uri, name, raw, addBaseURI(attr)); |
| else |
| super.startElement(uri, name, raw, attr); |
| } |
| } |
| |
| private boolean mustAddBaseAttr(){ |
| if(level != 1) |
| return false; |
| if(this.parent == null) |
| return false; |
| String parentBase = this.parent.parentBaseURI; |
| String currentBase = xmlBaseSupport.getCurrentBase(); |
| if(currentBase == null) |
| return false; |
| if(parentBase == null || !parentBase.equals(currentBase)) |
| return true; |
| return false; |
| } |
| |
| /** |
| * Adds xml:base attribute as per the XInclude spec. |
| */ |
| private Attributes addBaseURI(Attributes oldAttr) throws SAXException { |
| String currentBaseURI = xmlBaseSupport.getCurrentBase(); |
| |
| AttributesImpl fixedAttr = new AttributesImpl(oldAttr); |
| |
| // Old xml:base attributes are removed. |
| int xmlBaseAttrIdx = fixedAttr.getIndex(XMLBASE_NAMESPACE_URI, XMLBASE_ATTRIBUTE); |
| if(xmlBaseAttrIdx != -1) |
| fixedAttr.removeAttribute(xmlBaseAttrIdx); |
| |
| fixedAttr.addAttribute( |
| XMLBASE_NAMESPACE_URI, XMLBASE_ATTRIBUTE, |
| XMLBASE_NAMESPACE_PREFIX + ":" + XMLBASE_ATTRIBUTE, |
| XMLBASE_ATTRIBUTE_TYPE, |
| currentBaseURI |
| ); |
| return fixedAttr; |
| } |
| |
| public void endElement(String uri, String name, String raw) throws SAXException { |
| // Track xml:base context: |
| xmlBaseSupport.endElement(uri, name, raw); |
| this.level--; |
| |
| // Handle elements in xinclude namespace: |
| if (XINCLUDE_NAMESPACE_URI.equals(uri)) { |
| // Handle xi:include: |
| if (XINCLUDE_INCLUDE_ELEMENT.equals(name)) { |
| xIncludeElementLevel--; |
| if (useFallbackLevel > xIncludeElementLevel) { |
| useFallbackLevel = xIncludeElementLevel; |
| } |
| } else if (XINCLUDE_FALLBACK_ELEMENT.equals(name)) { |
| // Handle xi:fallback: |
| fallbackElementLevel--; |
| } |
| } else if (isEvaluatingContent()) { |
| // Copy other elements through when appropriate: |
| super.endElement(uri, name, raw); |
| } |
| } |
| |
| public void startPrefixMapping(String prefix, String uri) throws SAXException { |
| if (isEvaluatingContent()) { |
| super.startPrefixMapping(prefix, uri); |
| } |
| } |
| |
| public void endPrefixMapping(String prefix) throws SAXException { |
| if (isEvaluatingContent()) { |
| super.endPrefixMapping(prefix); |
| } |
| } |
| |
| public void characters(char c[], int start, int len) throws SAXException { |
| if (isEvaluatingContent()) { |
| super.characters(c, start, len); |
| } |
| } |
| |
| public void ignorableWhitespace(char c[], int start, int len) throws SAXException { |
| if (isEvaluatingContent()) { |
| super.ignorableWhitespace(c, start, len); |
| } |
| } |
| |
| public void processingInstruction(String target, String data) throws SAXException { |
| if (isEvaluatingContent()) { |
| super.processingInstruction(target, data); |
| } |
| } |
| |
| public void skippedEntity(String name) throws SAXException { |
| if (isEvaluatingContent()) { |
| super.skippedEntity(name); |
| } |
| } |
| |
| public void startEntity(String name) throws SAXException { |
| if (isEvaluatingContent()) { |
| super.startEntity(name); |
| } |
| } |
| |
| public void endEntity(String name) throws SAXException { |
| if (isEvaluatingContent()) { |
| super.endEntity(name); |
| } |
| } |
| |
| public void startCDATA() throws SAXException { |
| if (isEvaluatingContent()) { |
| super.startCDATA(); |
| } |
| } |
| |
| public void endCDATA() throws SAXException { |
| if (isEvaluatingContent()) { |
| super.endCDATA(); |
| } |
| } |
| |
| public void comment(char ch[], int start, int len) throws SAXException { |
| if (isEvaluatingContent()) { |
| super.comment(ch, start, len); |
| } |
| } |
| |
| public void setDocumentLocator(Locator locator) { |
| try { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("setDocumentLocator called " + locator.getSystemId()); |
| } |
| |
| // When using SAXON to serialize a DOM tree to SAX, a locator is passed with a "null" system id |
| if (locator.getSystemId() != null) { |
| Source source = resolver.resolveURI(locator.getSystemId()); |
| try { |
| xmlBaseSupport.setDocumentLocation(source.getURI()); |
| // only for the "root" XIncludePipe, we'll have to set the href here, in the other cases |
| // the href is taken from the xi:include href attribute |
| if (href == null) |
| href = source.getURI(); |
| } finally { |
| resolver.release(source); |
| } |
| } |
| } catch (Exception e) { |
| throw new CascadingRuntimeException("Error in XIncludeTransformer while trying to resolve base URL for document", e); |
| } |
| this.locator = locator; |
| super.setDocumentLocator(locator); |
| } |
| |
| protected void processXIncludeElement(String href, String parse, String xpointer) |
| throws SAXException,ProcessingException,IOException { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Processing XInclude element: href="+href+", parse="+parse+", xpointer="+xpointer); |
| } |
| |
| // Default for @parse is "xml" |
| if (parse == null) { |
| parse = "xml"; |
| } |
| Source url = null; |
| |
| try { |
| int fragmentIdentifierPos = href.indexOf('#'); |
| if (fragmentIdentifierPos != -1) { |
| getLogger().warn("Fragment identifer found in 'href' attribute: " + href + |
| "\nFragment identifiers are forbidden by the XInclude specification. " + |
| "They are still handled by XIncludeTransformer for backward " + |
| "compatibility, but their use is deprecated and will be prohibited " + |
| "in a future release. Use the 'xpointer' attribute instead."); |
| if (xpointer == null) { |
| xpointer = href.substring(fragmentIdentifierPos + 1); |
| } |
| href = href.substring(0, fragmentIdentifierPos); |
| } |
| |
| // An empty or absent href is a reference to the current document -- this can be different than the current base |
| if (href == null || href.length() == 0) { |
| if (this.href == null) { |
| throw new SAXException("XIncludeTransformer: encountered empty href (= href pointing to the current document) but the location of the current document is unknown."); |
| } |
| // The following can be simplified once fragment identifiers are prohibited |
| int fragmentIdentifierPos2 = this.href.indexOf('#'); |
| if (fragmentIdentifierPos2 != -1) |
| href = this.href.substring(0, fragmentIdentifierPos2); |
| else |
| href = this.href; |
| } |
| |
| url = xmlBaseSupport.makeAbsolute(href); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("URL: " + url.getURI() + "\nXPointer: " + xpointer); |
| } |
| |
| // add the source to the SourceValidity |
| validity.addSource(url); |
| |
| if (parse.equals("text")) { |
| getLogger().debug("Parse type is text"); |
| if (xpointer != null) { |
| throw new SAXException("xpointer attribute must not be present when parse='text': " + getLocation()); |
| } |
| InputStream is = null; |
| InputStreamReader isr = null; |
| Reader reader = null; |
| try { |
| is = url.getInputStream(); |
| isr = new InputStreamReader(is); |
| reader = new BufferedReader(isr); |
| int read; |
| char ary[] = new char[1024 * 4]; |
| while ((read = reader.read(ary)) != -1) { |
| super.characters(ary,0,read); |
| } |
| } catch (SourceNotFoundException e) { |
| useFallbackLevel++; |
| fallBackException = new CascadingException("Resource not found: " + url.getURI()); |
| getLogger().error("xIncluded resource not found: " + url.getURI(), e); |
| } finally { |
| if (reader != null) reader.close(); |
| if (isr != null) isr.close(); |
| if (is != null) is.close(); |
| } |
| } else if (parse.equals("xml")) { |
| getLogger().debug("Parse type is XML"); |
| |
| // Check loop inclusion |
| if (isLoopInclusion(url.getURI(), xpointer)) { |
| throw new ProcessingException("Detected loop inclusion of href=" + url.getURI() + ", xpointer=" + xpointer); |
| } |
| |
| XIncludePipe subPipe = new XIncludePipe(); |
| subPipe.enableLogging(getLogger()); |
| subPipe.init(url.getURI(), xpointer); |
| subPipe.setConsumer(xmlConsumer); |
| subPipe.setParent(this); |
| |
| try { |
| if (xpointer != null && xpointer.length() > 0) { |
| XPointer xptr; |
| xptr = XPointerFrameworkParser.parse(NetUtils.decodePath(xpointer)); |
| XPointerContext context = new XPointerContext(xpointer, url, subPipe, getLogger(), manager); |
| xptr.process(context); |
| } else { |
| SourceUtil.toSAX(url, new IncludeXMLConsumer(subPipe)); |
| } |
| // restore locator on the consumer |
| if (locator != null) |
| xmlConsumer.setDocumentLocator(locator); |
| } catch (ResourceNotFoundException e) { |
| useFallbackLevel++; |
| fallBackException = new CascadingException("Resource not found: " + url.getURI()); |
| getLogger().error("xIncluded resource not found: " + url.getURI(), e); |
| } catch (ParseException e) { |
| // this exception is thrown in case of an invalid xpointer expression |
| useFallbackLevel++; |
| fallBackException = new CascadingException("Error parsing xPointer expression", e); |
| fallBackException.fillInStackTrace(); |
| getLogger().error("Error parsing XPointer expression, will try to use fallback.", e); |
| } catch(SAXException e) { |
| getLogger().error("Error in processXIncludeElement", e); |
| throw e; |
| } catch(ProcessingException e) { |
| getLogger().error("Error in processXIncludeElement", e); |
| throw e; |
| } catch(MalformedURLException e) { |
| useFallbackLevel++; |
| fallBackException = e; |
| getLogger().error("Error processing an xInclude, will try to use fallback.", e); |
| } catch(IOException e) { |
| useFallbackLevel++; |
| fallBackException = e; |
| getLogger().error("Error processing an xInclude, will try to use fallback.", e); |
| } |
| } else { |
| throw new SAXException("Found 'parse' attribute with unknown value " + parse + " at " + getLocation()); |
| } |
| } catch (SourceException se) { |
| throw SourceUtil.handle(se); |
| } finally { |
| if (url != null) { |
| resolver.release(url); |
| } |
| } |
| } |
| |
| public boolean isLoopInclusion(String uri, String xpointer) { |
| if (xpointer == null) { |
| xpointer = ""; |
| } |
| |
| if (uri.equals(this.href) && xpointer.equals(this.xpointer == null ? "" : this.xpointer)) { |
| return true; |
| } |
| |
| XIncludePipe parent = getParent(); |
| while (parent != null) { |
| if (uri.equals(parent.getHref()) && xpointer.equals(parent.getXpointer() == null ? "" : parent.getXpointer())) { |
| return true; |
| } |
| parent = parent.getParent(); |
| } |
| return false; |
| } |
| |
| private String getLocation() { |
| if (this.locator == null) { |
| return "unknown location"; |
| } else { |
| return this.locator.getSystemId() + ":" + this.locator.getColumnNumber() + ":" + this.locator.getLineNumber(); |
| } |
| } |
| } |
| } |