/* * 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.pipeline.component.sax; import java.io.IOException; import java.net.URI; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import org.apache.cocoon.pipeline.component.xpointer.XPointer; import org.apache.cocoon.pipeline.component.xpointer.XPointerContext; import org.apache.cocoon.pipeline.component.xpointer.parser.ParseException; import org.apache.cocoon.pipeline.component.xpointer.parser.XPointerFrameworkParser; import org.richfaces.cdk.Logger; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.ext.EntityResolver2; import org.xml.sax.ext.LexicalHandler; import org.xml.sax.helpers.XMLReaderFactory; public final class XIncludeTransformer implements SAXConsumer { private static final String DEFAULT_CHARSET = "UTF-8"; private static final String HTTP_ACCEPT = "Accept"; private static final String HTTP_ACCEPT_LANGUAGE = "Accept-Language"; private static final String UNKNOWN_LOCATION = "unknow location"; private static final String XINCLUDE_ACCEPT = "accept"; private static final String XINCLUDE_ACCEPT_LANGUAGE = "accept-language"; private static final String XINCLUDE_ENCODING = "encoding"; private static final String XINCLUDE_FALLBACK = "fallback"; private static final String XINCLUDE_HREF = "href"; private static final String XINCLUDE_INCLUDE = "include"; private static final String XINCLUDE_NAMESPACE_URI = "http://www.w3.org/2001/XInclude"; private static final String XINCLUDE_PARSE = "parse"; private static final String XINCLUDE_PARSE_TEXT = "text"; private static final String XINCLUDE_PARSE_XML = "xml"; private static final String XINCLUDE_XPOINTER = "xpointer"; private final Logger log; /** * The nesting level of fallback that should be used */ private int useFallbackLevel = 0; /** * The nesting level of xi:include elements that have been encountered. */ private int xIncludeElementLevel = 0; /** * Keep a map of namespaces prefix in the source document to pass it to the XPointerContext for correct namespace * identification. */ private final Map<String, String> namespaces = new HashMap<String, String>(); private URI baseUri; private ContentHandler contentHandler; /** * The nesting level of xi:fallback elements that have been encountered. */ private int fallbackElementLevel; private LexicalHandler lexicalHandler; /** * 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; private EntityResolver2 resolver; public XIncludeTransformer(Logger log) { this.log = log; } public XIncludeTransformer(URI baseUri, Logger log) { this(log); this.setBaseUri(baseUri); } /** * <p class="changed_added_4_0"> * </p> * * @return the resolver */ public EntityResolver2 getResolver() { return resolver; } /** * <p class="changed_added_4_0"> * </p> * * @param resolver the resolver to set */ public void setResolver(EntityResolver2 resolver) { this.resolver = resolver; } /** * <p class="changed_added_4_0"> * </p> * * @return the contentHandler */ public ContentHandler getContentHandler() { return contentHandler; } /** * <p class="changed_added_4_0"> * </p> * * @return the lexicalHandler */ public LexicalHandler getLexicalHandler() { if (lexicalHandler == null) { lexicalHandler = new DummyLexicalHandler(); } return lexicalHandler; } public void setBaseUri(URI baseUri) { this.baseUri = baseUri; } /** * Eventually previous errors don't reset local variables status, so every time a new consumer is set, local variables * should be re-initialized */ public void setContentHandler(ContentHandler delegateHandler) { this.contentHandler = delegateHandler; if (delegateHandler instanceof LexicalHandler) { lexicalHandler = (LexicalHandler) delegateHandler; } this.xIncludeElementLevel = 0; this.fallbackElementLevel = 0; this.useFallbackLevel = 0; } /** * 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); } private String getLocation() { if (locator == null) { return UNKNOWN_LOCATION; } else { return locator.getSystemId() + ":" + locator.getColumnNumber() + ":" + locator.getLineNumber(); } } public void startDocument() throws SAXException { if (xIncludeElementLevel == 0) { getContentHandler().startDocument(); } } public void endDocument() throws SAXException { if (xIncludeElementLevel == 0) { getContentHandler().endDocument(); } } public void startElement(String uri, String localName, String name, Attributes atts) throws SAXException { if (XINCLUDE_NAMESPACE_URI.equals(uri)) { // Handle xi:include: if (XINCLUDE_INCLUDE.equals(localName)) { // Process the include, unless in an ignored fallback: if (isEvaluatingContent()) { String href = atts.getValue("", XINCLUDE_HREF); String parse = atts.getValue("", XINCLUDE_PARSE); // Default for @parse is "xml" if (parse == null) { parse = XINCLUDE_PARSE_XML; } String xpointer = atts.getValue("", XINCLUDE_XPOINTER); String encoding = atts.getValue("", XINCLUDE_ENCODING); String accept = atts.getValue("", XINCLUDE_ACCEPT); String acceptLanguage = atts.getValue("", XINCLUDE_ACCEPT_LANGUAGE); processXIncludeElement(href, parse, xpointer, encoding, accept, acceptLanguage); } xIncludeElementLevel++; } else if (XINCLUDE_FALLBACK.equals(localName)) { // Handle xi:fallback fallbackElementLevel++; } else { // Unknown element: throw new SAXException("Unknown XInclude element " + localName + " at " + getLocation()); } } else if (isEvaluatingContent()) { // Copy other elements through when appropriate: getContentHandler().startElement(uri, localName, name, atts); } } private void processXIncludeElement(String href, String parse, String xpointer, String encoding, String accept, String acceptLanguage) throws SAXException { if (log.isDebugEnabled()) { log.debug("Processing XInclude element: href=" + href + ", parse=" + parse + ", xpointer=" + xpointer + ", encoding=" + encoding + ", accept=" + accept + ", acceptLanguage=" + acceptLanguage); } int fragmentIdentifierPos = href.indexOf('#'); if (fragmentIdentifierPos != -1) { log.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) { throw new SAXException("XIncludeTransformer: encountered empty href (= href pointing to the current document)."); } InputSource source = createSource(href); if (log.isDebugEnabled()) { log.debug("Parse type=" + parse); } if (XINCLUDE_PARSE_XML.equals(parse)) { try { if (xpointer != null && xpointer.length() > 0) { XPointer xPointer = XPointerFrameworkParser.parse(xpointer); XPointerContext xPointerContext = new XPointerContext(xpointer, source, this, resolver, log); for (Entry<String, String> namespace : namespaces.entrySet()) { xPointerContext.addPrefix(namespace.getKey(), namespace.getValue()); } xPointer.process(xPointerContext); } else { // just parses the document and streams it XMLReader xmlReader = XMLReaderFactory.createXMLReader(); xmlReader.setContentHandler(this); xmlReader.setEntityResolver(resolver); xmlReader.setProperty("http://xml.org/sax/properties/lexical-handler", this); xmlReader.parse(source); } } catch (ParseException e) { // this exception is thrown in case of an invalid xpointer expression useFallbackLevel++; log.info("Error parsing XPointer expression: " + e.getMessage() + " , will try to use fallback."); } catch (IOException e) { useFallbackLevel++; log.info("Error processing an xInclude: " + e.getMessage() + " will try to use fallback."); } catch (SAXException e) { useFallbackLevel++; log.info("Error processing an xInclude: " + e.getMessage() + " will try to use fallback."); } } else if (XINCLUDE_PARSE_TEXT.equals(parse)) { if (xpointer != null) { throw new SAXException("xpointer attribute must not be present when parse='text': " + getLocation()); } // TODO - read source as text. if (null != source.getCharacterStream()) { // use reader } else if (null != source.getByteStream()) { // use stream, detect encoding } else if (null != source.getSystemId()) { // get from url. } else { useFallbackLevel++; log.error("Can't read XInclude href " + href + " at " + getLocation()); } } else { throw new SAXException("Found 'parse' attribute with unknown value " + parse + " at " + getLocation()); } } private InputSource createSource(String sourceAtt) throws SAXException { try { InputSource source = null; URI sourceURI = URI.create(sourceAtt); if (!sourceURI.isAbsolute() && null != this.baseUri) { sourceAtt = this.baseUri.resolve(sourceURI).toString(); } if (null != resolver) { source = resolver.resolveEntity(null, sourceAtt); } if (null == source) { source = new InputSource(sourceAtt); } if (this.log.isDebugEnabled()) { this.log.debug("Including source: " + sourceAtt); } return source; } catch (IllegalArgumentException e) { String message = "Invalid xinclude URI " + sourceAtt; this.log.error(message, e); throw new ProcessingException(message, e); } catch (IOException e) { String message = "Can't resolve URL " + sourceAtt; this.log.error(message, e); throw new ProcessingException(message, e); } } public void endElement(String uri, String localName, String name) throws SAXException { // Handle elements in xinclude namespace: if (XINCLUDE_NAMESPACE_URI.equals(uri)) { // Handle xi:include: if (XINCLUDE_INCLUDE.equals(localName)) { xIncludeElementLevel--; if (useFallbackLevel > xIncludeElementLevel) { useFallbackLevel = xIncludeElementLevel; } } else if (XINCLUDE_FALLBACK.equals(localName)) { // Handle xi:fallback: fallbackElementLevel--; } } else if (isEvaluatingContent()) { // Copy other elements through when appropriate: getContentHandler().endElement(uri, localName, name); } } public void startPrefixMapping(String prefix, String uri) throws SAXException { if (isEvaluatingContent()) { // removed xinclude namespace from result document if (!uri.equals(XINCLUDE_NAMESPACE_URI)) { getContentHandler().startPrefixMapping(prefix, uri); } namespaces.put(prefix, uri); } } public void endPrefixMapping(String prefix) throws SAXException { if (isEvaluatingContent()) { String uri = namespaces.remove(prefix); if (!XINCLUDE_NAMESPACE_URI.equals(uri)) { getContentHandler().endPrefixMapping(prefix); } } } public void startCDATA() throws SAXException { if (isEvaluatingContent()) { getLexicalHandler().startCDATA(); } } public void endCDATA() throws SAXException { if (isEvaluatingContent()) { getLexicalHandler().startCDATA(); } } public void startDTD(String name, String publicId, String systemId) throws SAXException { // ignoring DTD } public void endDTD() throws SAXException { // ignoring DTD } public void startEntity(String name) throws SAXException { if (isEvaluatingContent()) { getLexicalHandler().startEntity(name); } } public void endEntity(String name) throws SAXException { if (isEvaluatingContent()) { getLexicalHandler().endEntity(name); } } public void characters(char[] ch, int start, int length) throws SAXException { if (isEvaluatingContent()) { getContentHandler().characters(ch, start, length); } } public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { if (isEvaluatingContent()) { getContentHandler().ignorableWhitespace(ch, start, length); } } public void comment(char[] ch, int start, int length) throws SAXException { // skip comments } public void processingInstruction(String target, String data) throws SAXException { if (isEvaluatingContent()) { getContentHandler().processingInstruction(target, data); } } public void setDocumentLocator(Locator locator) { if (log.isDebugEnabled()) { log.debug("setDocumentLocator called " + locator.getSystemId()); } this.locator = locator; getContentHandler().setDocumentLocator(locator); } public void skippedEntity(String name) throws SAXException { if (isEvaluatingContent()) { getContentHandler().skippedEntity(name); } } private static final class DummyLexicalHandler implements LexicalHandler { public void startEntity(String name) throws SAXException { } public void startDTD(String name, String publicId, String systemId) throws SAXException { } public void startCDATA() throws SAXException { } public void endEntity(String name) throws SAXException { } public void endDTD() throws SAXException { } public void endCDATA() throws SAXException { } public void comment(char[] ch, int start, int length) throws SAXException { } } }