| /* |
| * 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 java.security.AccessController; |
| import java.security.PrivilegedAction; |
| |
| 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.accessibility.fo.FO2StructureTreeConverter; |
| import org.apache.fop.apps.FOPException; |
| import org.apache.fop.apps.FOUserAgent; |
| import org.apache.fop.apps.FormattingResults; |
| 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.render.pdf.extensions.PDFElementMapping; |
| 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 */ |
| private static final Log LOG = LogFactory.getLog(FOTreeBuilder.class); |
| |
| /** The registry for ElementMapping instances */ |
| protected ElementMappingRegistry elementMappingRegistry; |
| |
| /** The root of the formatting object tree */ |
| protected Root rootFObj; |
| |
| /** 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; |
| private boolean empty = true; |
| |
| private int depth; |
| private boolean errorinstart; |
| |
| /** |
| * <code>FOTreeBuilder</code> 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 <code>OutputStream</code> to direct the results to |
| * @throws FOPException if the <code>FOTreeBuilder</code> cannot be properly created |
| */ |
| public FOTreeBuilder( |
| String outputFormat, |
| FOUserAgent foUserAgent, |
| OutputStream stream) |
| throws FOPException { |
| |
| this.userAgent = foUserAgent; |
| this.elementMappingRegistry = userAgent.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); |
| if (userAgent.isAccessibilityEnabled()) { |
| foEventHandler = new FO2StructureTreeConverter( |
| foUserAgent.getStructureTreeEventHandler(), foEventHandler); |
| } |
| 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(final String namespaceURI, final String localName, final String rawName, |
| final Attributes attlist) throws SAXException { |
| this.depth++; |
| errorinstart = false; |
| final ContentHandler contentHandler = delegate; |
| SAXException saxException = AccessController.doPrivileged( |
| new PrivilegedAction<SAXException>() { |
| public SAXException run() { |
| try { |
| contentHandler.startElement(namespaceURI, localName, rawName, attlist); |
| } catch (SAXException e) { |
| return e; |
| } |
| return null; |
| } |
| } |
| ); |
| if (saxException != null) { |
| errorinstart = true; |
| throw saxException; |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public void endElement(final String uri, final String localName, final String rawName) throws SAXException { |
| if (!errorinstart) { |
| final ContentHandler contentHandler = delegate; |
| SAXException saxException = AccessController.doPrivileged( |
| new PrivilegedAction<SAXException>() { |
| public SAXException run() { |
| try { |
| contentHandler.endElement(uri, localName, rawName); |
| } catch (SAXException e) { |
| return e; |
| } |
| return null; |
| } |
| } |
| ); |
| if (saxException != null) { |
| throw saxException; |
| } |
| |
| 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() { |
| return getEventHandler().getResults(); |
| } |
| |
| /** |
| * Main <code>DefaultHandler</code> implementation which builds the FO tree. |
| */ |
| private class MainFOHandler extends DefaultHandler { |
| |
| /** Current formatting object being handled */ |
| protected FONode currentFObj; |
| |
| /** Current propertyList for the node being handled */ |
| protected PropertyList currentPropertyList; |
| |
| /** Current marker nesting-depth */ |
| private int nestedMarkerDepth; |
| |
| /** {@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.getNamespaceURI().equals(PDFElementMapping.NAMESPACE)) { |
| if (!currentFObj.isChangeBarElement(namespaceURI, localName)) { |
| 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 |
| && (!builderContext.inMarker() || currentFObj.getNameId() == Constants.FO_MARKER)) { |
| 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 |
| && (!builderContext.inMarker() || currentFObj.getNameId() == Constants.FO_MARKER)) { |
| 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()); |
| String name = (currentFObj != null ? currentFObj.getName() |
| : "{" + namespaceURI + "}" + localName); |
| eventProducer.unknownFormattingObject(this, name, |
| new QName(namespaceURI, localName), |
| getEffectiveLocator()); |
| } |
| return maker; |
| } |
| |
| } |
| } |
| |