blob: 8c5aa89b17ad55734c14f8cee2183914f20d9e2f [file] [log] [blame]
/*
* 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();
}
}
}
}