/*
* 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.
*/
/* $Id$ */
package org.apache.fop.fo;
import java.io.OutputStream;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xmlgraphics.util.QName;
import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.apps.FormattingResults;
import org.apache.fop.area.AreaTreeHandler;
import org.apache.fop.fo.ElementMapping.Maker;
import org.apache.fop.fo.extensions.ExtensionElementMapping;
import org.apache.fop.fo.pagination.Root;
import org.apache.fop.util.ContentHandlerFactory;
import org.apache.fop.util.ContentHandlerFactory.ObjectBuiltListener;
import org.apache.fop.util.ContentHandlerFactory.ObjectSource;
/**
* SAX Handler that passes parsed data to the various
* FO objects, where they can be used either to build
* an FO Tree, or used by Structure Renderers to build
* other data structures.
*/
public class FOTreeBuilder extends DefaultHandler {
/** logging instance */
protected Log log = LogFactory.getLog(FOTreeBuilder.class);
/** The registry for ElementMapping instances */
protected ElementMappingRegistry elementMappingRegistry;
/** The root of the formatting object tree */
protected Root rootFObj = null;
/** Main DefaultHandler that handles the FO namespace. */
protected MainFOHandler mainFOHandler;
/** Current delegate ContentHandler to receive the SAX events */
protected ContentHandler delegate;
/** Provides information used during tree building stage. */
private FOTreeBuilderContext builderContext;
/** The object that handles formatting and rendering to a stream */
private FOEventHandler foEventHandler;
/** The SAX locator object managing the line and column counters */
private Locator locator;
/** The user agent for this processing run. */
private FOUserAgent userAgent;
private boolean used = false;
private boolean empty = true;
private int depth;
/**
* FOTreeBuilder
constructor
*
* @param outputFormat the MIME type of the output format to use (ex. "application/pdf").
* @param foUserAgent the {@link FOUserAgent} in effect for this process
* @param stream the OutputStream
to direct the results to
* @throws FOPException if the FOTreeBuilder
cannot be properly created
*/
public FOTreeBuilder(
String outputFormat,
FOUserAgent foUserAgent,
OutputStream stream)
throws FOPException {
this.userAgent = foUserAgent;
this.elementMappingRegistry = userAgent.getFactory().getElementMappingRegistry();
//This creates either an AreaTreeHandler and ultimately a Renderer, or
//one of the RTF-, MIF- etc. Handlers.
foEventHandler = foUserAgent.getRendererFactory().createFOEventHandler(
foUserAgent, outputFormat, stream);
builderContext = new FOTreeBuilderContext();
builderContext.setPropertyListMaker(new PropertyListMaker() {
public PropertyList make(FObj fobj, PropertyList parentPropertyList) {
return new StaticPropertyList(fobj, parentPropertyList);
}
});
}
/** {@inheritDoc} */
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
/**
* @return a {@link Locator} instance if it is available and not disabled
*/
protected Locator getEffectiveLocator() {
return (userAgent.isLocatorEnabled() ? this.locator : null);
}
/** {@inheritDoc} */
public void characters(char[] data, int start, int length)
throws SAXException {
delegate.characters(data, start, length);
}
/** {@inheritDoc} */
public void startDocument() throws SAXException {
if (used) {
throw new IllegalStateException("FOTreeBuilder (and the Fop class) cannot be reused."
+ " Please instantiate a new instance.");
}
used = true;
empty = true;
rootFObj = null; // allows FOTreeBuilder to be reused
if (log.isDebugEnabled()) {
log.debug("Building formatting object tree");
}
foEventHandler.startDocument();
this.mainFOHandler = new MainFOHandler();
this.mainFOHandler.startDocument();
this.delegate = this.mainFOHandler;
}
/** {@inheritDoc} */
public void endDocument() throws SAXException {
this.delegate.endDocument();
if (this.rootFObj == null && empty) {
FOValidationEventProducer eventProducer
= FOValidationEventProducer.Provider.get(userAgent.getEventBroadcaster());
eventProducer.emptyDocument(this);
}
rootFObj = null;
if (log.isDebugEnabled()) {
log.debug("Parsing of document complete");
}
foEventHandler.endDocument();
}
/** {@inheritDoc} */
public void startElement(String namespaceURI, String localName, String rawName,
Attributes attlist) throws SAXException {
this.depth++;
delegate.startElement(namespaceURI, localName, rawName, attlist);
}
/** {@inheritDoc} */
public void endElement(String uri, String localName, String rawName)
throws SAXException {
this.delegate.endElement(uri, localName, rawName);
this.depth--;
if (depth == 0) {
if (delegate != mainFOHandler) {
//Return from sub-handler back to main handler
delegate.endDocument();
delegate = mainFOHandler;
delegate.endElement(uri, localName, rawName);
}
}
}
/** {@inheritDoc} */
public void warning(SAXParseException e) {
log.warn(e.getLocalizedMessage());
}
/** {@inheritDoc} */
public void error(SAXParseException e) {
log.error(e.toString());
}
/** {@inheritDoc} */
public void fatalError(SAXParseException e) throws SAXException {
log.error(e.toString());
throw e;
}
/**
* Provides access to the underlying {@link FOEventHandler} object.
*
* @return the FOEventHandler object
*/
public FOEventHandler getEventHandler() {
return foEventHandler;
}
/**
* Returns the results of the rendering process. Information includes
* the total number of pages generated and the number of pages per
* page-sequence.
*
* @return the results of the rendering process.
*/
public FormattingResults getResults() {
if (getEventHandler() instanceof AreaTreeHandler) {
return ((AreaTreeHandler) getEventHandler()).getResults();
} else {
//No formatting results available for output formats no
//involving the layout engine.
return null;
}
}
/**
* Main DefaultHandler
implementation which builds the FO tree.
*/
private class MainFOHandler extends DefaultHandler {
/** Current formatting object being handled */
protected FONode currentFObj = null;
/** Current propertyList for the node being handled */
protected PropertyList currentPropertyList;
/** Current marker nesting-depth */
private int nestedMarkerDepth = 0;
/** {@inheritDoc} */
public void startElement(String namespaceURI, String localName, String rawName,
Attributes attlist) throws SAXException {
/* the node found in the FO document */
FONode foNode;
PropertyList propertyList = null;
// Check to ensure first node encountered is an fo:root
if (rootFObj == null) {
empty = false;
if (!namespaceURI.equals(FOElementMapping.URI)
|| !localName.equals("root")) {
FOValidationEventProducer eventProducer
= FOValidationEventProducer.Provider.get(
userAgent.getEventBroadcaster());
eventProducer.invalidFORoot(this, FONode.getNodeString(namespaceURI, localName),
getEffectiveLocator());
}
} else { // check that incoming node is valid for currentFObj
if (currentFObj.getNamespaceURI().equals(FOElementMapping.URI)
|| currentFObj.getNamespaceURI().equals(ExtensionElementMapping.URI)) {
currentFObj.validateChildNode(locator, namespaceURI, localName);
}
}
ElementMapping.Maker fobjMaker = findFOMaker(namespaceURI, localName);
try {
foNode = fobjMaker.make(currentFObj);
if (rootFObj == null) {
rootFObj = (Root) foNode;
rootFObj.setBuilderContext(builderContext);
rootFObj.setFOEventHandler(foEventHandler);
}
propertyList = foNode.createPropertyList(
currentPropertyList, foEventHandler);
foNode.processNode(localName, getEffectiveLocator(),
attlist, propertyList);
if (foNode.getNameId() == Constants.FO_MARKER) {
if (builderContext.inMarker()) {
nestedMarkerDepth++;
} else {
builderContext.switchMarkerContext(true);
}
}
if (foNode.getNameId() == Constants.FO_PAGE_SEQUENCE) {
builderContext.getXMLWhiteSpaceHandler().reset();
}
} catch (IllegalArgumentException e) {
throw new SAXException(e);
}
ContentHandlerFactory chFactory = foNode.getContentHandlerFactory();
if (chFactory != null) {
ContentHandler subHandler = chFactory.createContentHandler();
if (subHandler instanceof ObjectSource
&& foNode instanceof ObjectBuiltListener) {
((ObjectSource) subHandler).setObjectBuiltListener(
(ObjectBuiltListener) foNode);
}
subHandler.startDocument();
subHandler.startElement(namespaceURI, localName,
rawName, attlist);
depth = 1;
delegate = subHandler;
}
if (currentFObj != null) {
currentFObj.addChildNode(foNode);
}
currentFObj = foNode;
if (propertyList != null && !builderContext.inMarker()) {
currentPropertyList = propertyList;
}
// fo:characters can potentially be removed during
// white-space handling.
// Do not notify the FOEventHandler.
if (currentFObj.getNameId() != Constants.FO_CHARACTER) {
currentFObj.startOfNode();
}
}
/** {@inheritDoc} */
public void endElement(String uri, String localName, String rawName)
throws SAXException {
if (currentFObj == null) {
throw new SAXException(
"endElement() called for " + rawName
+ " where there is no current element.");
} else if (!currentFObj.getLocalName().equals(localName)
|| !currentFObj.getNamespaceURI().equals(uri)) {
throw new SAXException("Mismatch: " + currentFObj.getLocalName()
+ " (" + currentFObj.getNamespaceURI()
+ ") vs. " + localName + " (" + uri + ")");
}
// fo:characters can potentially be removed during
// white-space handling.
// Do not notify the FOEventHandler.
if (currentFObj.getNameId() != Constants.FO_CHARACTER) {
currentFObj.endOfNode();
}
if (currentPropertyList != null
&& currentPropertyList.getFObj() == currentFObj
&& !builderContext.inMarker()) {
currentPropertyList = currentPropertyList.getParentPropertyList();
}
if (currentFObj.getNameId() == Constants.FO_MARKER) {
if (nestedMarkerDepth == 0) {
builderContext.switchMarkerContext(false);
} else {
nestedMarkerDepth--;
}
}
if (currentFObj.getParent() == null) {
log.debug("endElement for top-level " + currentFObj.getName());
}
currentFObj = currentFObj.getParent();
}
/** {@inheritDoc} */
public void characters(char[] data, int start, int length)
throws FOPException {
if (currentFObj != null) {
currentFObj.characters(data, start, length,
currentPropertyList, getEffectiveLocator());
}
}
/** {@inheritDoc} */
public void endDocument() throws SAXException {
currentFObj = null;
}
/**
* Finds the {@link Maker} used to create {@link FONode} objects of a particular type
*
* @param namespaceURI URI for the namespace of the element
* @param localName name of the Element
* @return the ElementMapping.Maker that can create an FO object for this element
* @throws FOPException if a Maker could not be found for a bound namespace.
*/
private Maker findFOMaker(String namespaceURI, String localName) throws FOPException {
Maker maker = elementMappingRegistry.findFOMaker(namespaceURI, localName, locator);
if (maker instanceof UnknownXMLObj.Maker) {
FOValidationEventProducer eventProducer
= FOValidationEventProducer.Provider.get(
userAgent.getEventBroadcaster());
eventProducer.unknownFormattingObject(this, currentFObj.getName(),
new QName(namespaceURI, localName),
getEffectiveLocator());
}
return maker;
}
}
}