From: Vincent Hennebert Date: Thu, 24 Nov 2011 17:15:28 +0000 (+0000) Subject: Removed the DOM representation of the structure tree. X-Git-Tag: fop-1_1rc1old~133^2~3 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=c6fb066a02573904f7ca404605f14c800adf80c5;p=xmlgraphics-fop.git Removed the DOM representation of the structure tree. The structure tree is now directly converted into corresponding PDF objects. When going the IF route, the structure tree is stored in the form of SAX events that will be re-played when it's time to stream them into the output. This may still change. * Extracted RFC3066 methods from XMLUtil into new LanguageTags class that can be re-used in a non-XML context. * Dropped support for accessibility in the old Area Tree XML. * Added support for the xml:lang property on fo:root, so that the global language can be set for the document without retrieving the language from the first page-sequence. * Renamed StructureTreeBuilder into more appropriate StructureTreeEventHandler (same for applicable sub-classes) * Renamed StructureTreeBuildingFOEventHandler into FO2StructureTreeConverter and added test case * Added test cases for classes in the StructureTreeEventHandler hierarchy git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/branches/Temp_ImproveAccessibility@1205935 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/src/documentation/intermediate-format-ng/fop-intermediate-format-ng.xsd b/src/documentation/intermediate-format-ng/fop-intermediate-format-ng.xsd index b6fa00bd2..86dce1a3c 100644 --- a/src/documentation/intermediate-format-ng/fop-intermediate-format-ng.xsd +++ b/src/documentation/intermediate-format-ng/fop-intermediate-format-ng.xsd @@ -40,10 +40,17 @@ - - - - + + + + + + + + + + + diff --git a/src/java/org/apache/fop/accessibility/DummyStructureTreeEventHandler.java b/src/java/org/apache/fop/accessibility/DummyStructureTreeEventHandler.java new file mode 100644 index 000000000..80a011bab --- /dev/null +++ b/src/java/org/apache/fop/accessibility/DummyStructureTreeEventHandler.java @@ -0,0 +1,52 @@ +/* + * 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.accessibility; + +import java.util.Locale; + +import org.xml.sax.Attributes; + +/** + * This implementation ignores all structure tree events. + */ +public final class DummyStructureTreeEventHandler implements StructureTreeEventHandler { + + /** The singleton instance of this class. */ + public static final StructureTreeEventHandler INSTANCE = new DummyStructureTreeEventHandler(); + + private DummyStructureTreeEventHandler() { } + + /** {@inheritDoc} */ + public void startPageSequence(Locale locale) { + } + + /** {@inheritDoc} */ + public void startNode(String name, Attributes attributes) { + } + + /** {@inheritDoc} */ + public void endNode(String name) { + } + + /** {@inheritDoc} */ + public void endPageSequence() { + } + +} diff --git a/src/java/org/apache/fop/accessibility/FO2StructureTreeConverter.java b/src/java/org/apache/fop/accessibility/FO2StructureTreeConverter.java new file mode 100644 index 000000000..e6d0193b9 --- /dev/null +++ b/src/java/org/apache/fop/accessibility/FO2StructureTreeConverter.java @@ -0,0 +1,823 @@ +/* + * 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.accessibility; + +import java.util.Locale; + +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +import org.apache.fop.apps.FOUserAgent; +import org.apache.fop.fo.DelegatingFOEventHandler; +import org.apache.fop.fo.FOEventHandler; +import org.apache.fop.fo.FONode; +import org.apache.fop.fo.extensions.ExtensionElementMapping; +import org.apache.fop.fo.extensions.ExternalDocument; +import org.apache.fop.fo.extensions.InternalElementMapping; +import org.apache.fop.fo.flow.AbstractGraphics; +import org.apache.fop.fo.flow.BasicLink; +import org.apache.fop.fo.flow.Block; +import org.apache.fop.fo.flow.BlockContainer; +import org.apache.fop.fo.flow.Character; +import org.apache.fop.fo.flow.ExternalGraphic; +import org.apache.fop.fo.flow.Footnote; +import org.apache.fop.fo.flow.FootnoteBody; +import org.apache.fop.fo.flow.Inline; +import org.apache.fop.fo.flow.InstreamForeignObject; +import org.apache.fop.fo.flow.Leader; +import org.apache.fop.fo.flow.ListBlock; +import org.apache.fop.fo.flow.ListItem; +import org.apache.fop.fo.flow.ListItemBody; +import org.apache.fop.fo.flow.ListItemLabel; +import org.apache.fop.fo.flow.PageNumber; +import org.apache.fop.fo.flow.PageNumberCitation; +import org.apache.fop.fo.flow.PageNumberCitationLast; +import org.apache.fop.fo.flow.Wrapper; +import org.apache.fop.fo.flow.table.Table; +import org.apache.fop.fo.flow.table.TableBody; +import org.apache.fop.fo.flow.table.TableCell; +import org.apache.fop.fo.flow.table.TableColumn; +import org.apache.fop.fo.flow.table.TableFooter; +import org.apache.fop.fo.flow.table.TableHeader; +import org.apache.fop.fo.flow.table.TableRow; +import org.apache.fop.fo.pagination.Flow; +import org.apache.fop.fo.pagination.PageSequence; +import org.apache.fop.fo.pagination.Root; +import org.apache.fop.fo.pagination.StaticContent; +import org.apache.fop.fo.properties.CommonAccessibilityHolder; +import org.apache.fop.util.XMLUtil; + +/** + * Allows to create the structure tree of an FO document, by converting FO + * events into appropriate structure tree events. + */ +public class FO2StructureTreeConverter extends DelegatingFOEventHandler { + + private int idCounter; + + /** Delegates to either {@link #foToStructureTreeEventAdapter} or {@link #eventSwallower}. */ + private FOEventHandler converter; + + private final FOEventHandler foToStructureTreeEventAdapter; + + /** The descendants of some elements like fo:leader must be ignored. */ + private final FOEventHandler eventSwallower; + + private final StructureTreeEventHandler structureTreeEventHandler; + + private final class FOToStructureTreeEventAdapter extends FOEventHandler { + + public FOToStructureTreeEventAdapter(FOUserAgent foUserAgent) { + super(foUserAgent); + } + + @Override + public void startDocument() throws SAXException { + } + + @Override + public void endDocument() throws SAXException { + } + + @Override + public void startPageSequence(PageSequence pageSeq) { + Locale locale = null; + if (pageSeq.getLanguage() != null) { + if (pageSeq.getCountry() != null) { + locale = new Locale(pageSeq.getLanguage(), pageSeq.getCountry()); + } else { + locale = new Locale(pageSeq.getLanguage()); + } + } + structureTreeEventHandler.startPageSequence(locale); + } + + @Override + public void endPageSequence(PageSequence pageSeq) { + structureTreeEventHandler.endPageSequence(); + } + + @Override + public void startPageNumber(PageNumber pagenum) { + startElementWithID(pagenum); + } + + @Override + public void endPageNumber(PageNumber pagenum) { + endElement(pagenum); + } + + @Override + public void startPageNumberCitation(PageNumberCitation pageCite) { + startElementWithID(pageCite); + } + + @Override + public void endPageNumberCitation(PageNumberCitation pageCite) { + endElement(pageCite); + } + + @Override + public void startPageNumberCitationLast(PageNumberCitationLast pageLast) { + startElementWithID(pageLast); + } + + @Override + public void endPageNumberCitationLast(PageNumberCitationLast pageLast) { + endElement(pageLast); + } + + @Override + public void startFlow(Flow fl) { + startElement(fl); + } + + @Override + public void endFlow(Flow fl) { + endElement(fl); + } + + @Override + public void startBlock(Block bl) { + startElementWithID(bl); + } + + @Override + public void endBlock(Block bl) { + endElement(bl); + } + + @Override + public void startBlockContainer(BlockContainer blc) { + startElement(blc); + } + + @Override + public void endBlockContainer(BlockContainer blc) { + endElement(blc); + } + + @Override + public void startInline(Inline inl) { + startElementWithID(inl); + } + + @Override + public void endInline(Inline inl) { + endElement(inl); + } + + @Override + public void startTable(Table tbl) { + startElementWithID(tbl); + } + + @Override + public void endTable(Table tbl) { + endElement(tbl); + } + + @Override + public void startHeader(TableHeader header) { + startElementWithID(header); + } + + @Override + public void endHeader(TableHeader header) { + endElement(header); + } + + @Override + public void startFooter(TableFooter footer) { + startElementWithID(footer); + } + + @Override + public void endFooter(TableFooter footer) { + endElement(footer); + } + + @Override + public void startBody(TableBody body) { + startElementWithID(body); + } + + @Override + public void endBody(TableBody body) { + endElement(body); + } + + @Override + public void startRow(TableRow tr) { + startElementWithID(tr); + } + + @Override + public void endRow(TableRow tr) { + endElement(tr); + } + + @Override + public void startCell(TableCell tc) { + AttributesImpl attributes = new AttributesImpl(); + int colSpan = tc.getNumberColumnsSpanned(); + if (colSpan > 1) { + addNoNamespaceAttribute(attributes, "number-columns-spanned", + Integer.toString(colSpan)); + } + startElementWithID(tc, attributes); + } + + @Override + public void endCell(TableCell tc) { + endElement(tc); + } + + @Override + public void startList(ListBlock lb) { + startElement(lb); + } + + @Override + public void endList(ListBlock lb) { + endElement(lb); + } + + @Override + public void startListItem(ListItem li) { + startElement(li); + } + + @Override + public void endListItem(ListItem li) { + endElement(li); + } + + @Override + public void startListLabel(ListItemLabel listItemLabel) { + startElement(listItemLabel); + } + + @Override + public void endListLabel(ListItemLabel listItemLabel) { + endElement(listItemLabel); + } + + @Override + public void startListBody(ListItemBody listItemBody) { + startElement(listItemBody); + } + + @Override + public void endListBody(ListItemBody listItemBody) { + endElement(listItemBody); + } + + @Override + public void startStatic(StaticContent staticContent) { + startElement(staticContent); + } + + @Override + public void endStatic(StaticContent statisContent) { + endElement(statisContent); + } + + @Override + public void startLink(BasicLink basicLink) { + startElementWithID(basicLink); + } + + @Override + public void endLink(BasicLink basicLink) { + endElement(basicLink); + } + + @Override + public void image(ExternalGraphic eg) { + startElementWithIDAndAltText(eg); + endElement(eg); + } + + @Override + public void startInstreamForeignObject(InstreamForeignObject ifo) { + startElementWithIDAndAltText(ifo); + } + + @Override + public void endInstreamForeignObject(InstreamForeignObject ifo) { + endElement(ifo); + } + + @Override + public void startFootnote(Footnote footnote) { + startElement(footnote); + } + + @Override + public void endFootnote(Footnote footnote) { + endElement(footnote); + } + + @Override + public void startFootnoteBody(FootnoteBody body) { + startElement(body); + } + + @Override + public void endFootnoteBody(FootnoteBody body) { + endElement(body); + } + + @Override + public void startWrapper(Wrapper wrapper) { + startElement(wrapper); + } + + @Override + public void endWrapper(Wrapper wrapper) { + endElement(wrapper); + } + + @Override + public void character(Character c) { + startElementWithID(c); + endElement(c); + } + + + private void startElement(FONode node) { + startElement(node, new AttributesImpl()); + } + + private void startElementWithID(FONode node) { + startElementWithID(node, new AttributesImpl()); + } + + private void startElementWithIDAndAltText(AbstractGraphics node) { + AttributesImpl attributes = new AttributesImpl(); + addAttribute(attributes, ExtensionElementMapping.URI, "alt-text", + ExtensionElementMapping.STANDARD_PREFIX, node.getAltText()); + startElementWithID(node, attributes); + } + + private void startElementWithID(FONode node, AttributesImpl attributes) { + String id = Integer.toHexString(idCounter++); + node.setPtr(id); + addAttribute(attributes, + InternalElementMapping.URI, "ptr", InternalElementMapping.STANDARD_PREFIX, id); + startElement(node, attributes); + } + + private void startElement(FONode node, AttributesImpl attributes) { + String localName = node.getLocalName(); + if (node instanceof CommonAccessibilityHolder) { + addRole((CommonAccessibilityHolder) node, attributes); + } + structureTreeEventHandler.startNode(localName, attributes); + } + + private void addNoNamespaceAttribute(AttributesImpl attributes, String name, String value) { + attributes.addAttribute("", name, name, XMLUtil.CDATA, value); + } + + private void addAttribute(AttributesImpl attributes, + String namespace, String localName, String prefix, String value) { + assert namespace.length() > 0 && prefix.length() > 0; + String qualifiedName = prefix + ":" + localName; + attributes.addAttribute(namespace, localName, qualifiedName, XMLUtil.CDATA, value); + } + + private void addRole(CommonAccessibilityHolder node, AttributesImpl attributes) { + String role = node.getCommonAccessibility().getRole(); + if (role != null) { + addNoNamespaceAttribute(attributes, "role", role); + } + } + + private void endElement(FONode node) { + String localName = node.getLocalName(); + structureTreeEventHandler.endNode(localName); + } + + } + + /** + * Creates a new instance. + * + * @param structureTreeEventHandler the object that will hold the structure tree + * @param delegate the FO event handler that must be wrapped by this instance + */ + public FO2StructureTreeConverter(StructureTreeEventHandler structureTreeEventHandler, + FOEventHandler delegate) { + super(delegate); + this.structureTreeEventHandler = structureTreeEventHandler; + this.foToStructureTreeEventAdapter = new FOToStructureTreeEventAdapter(foUserAgent); + this.converter = foToStructureTreeEventAdapter; + this.eventSwallower = new FOEventHandler(foUserAgent) { }; + } + + @Override + public void startDocument() throws SAXException { + converter.startDocument(); + super.startDocument(); + } + + @Override + public void endDocument() throws SAXException { + converter.endDocument(); + super.endDocument(); + } + + @Override + public void startRoot(Root root) { + converter.startRoot(root); + super.startRoot(root); + } + + @Override + public void endRoot(Root root) { + converter.endRoot(root); + super.endRoot(root); + } + + @Override + public void startPageSequence(PageSequence pageSeq) { + converter.startPageSequence(pageSeq); + super.startPageSequence(pageSeq); + } + + @Override + public void endPageSequence(PageSequence pageSeq) { + converter.endPageSequence(pageSeq); + super.endPageSequence(pageSeq); + } + + @Override + public void startPageNumber(PageNumber pagenum) { + converter.startPageNumber(pagenum); + super.startPageNumber(pagenum); + } + + @Override + public void endPageNumber(PageNumber pagenum) { + converter.endPageNumber(pagenum); + super.endPageNumber(pagenum); + } + + @Override + public void startPageNumberCitation(PageNumberCitation pageCite) { + converter.startPageNumberCitation(pageCite); + super.startPageNumberCitation(pageCite); + } + + @Override + public void endPageNumberCitation(PageNumberCitation pageCite) { + converter.endPageNumberCitation(pageCite); + super.endPageNumberCitation(pageCite); + } + + @Override + public void startPageNumberCitationLast(PageNumberCitationLast pageLast) { + converter.startPageNumberCitationLast(pageLast); + super.startPageNumberCitationLast(pageLast); + } + + @Override + public void endPageNumberCitationLast(PageNumberCitationLast pageLast) { + converter.endPageNumberCitationLast(pageLast); + super.endPageNumberCitationLast(pageLast); + } + + @Override + public void startFlow(Flow fl) { + converter.startFlow(fl); + super.startFlow(fl); + } + + @Override + public void endFlow(Flow fl) { + converter.endFlow(fl); + super.endFlow(fl); + } + + @Override + public void startBlock(Block bl) { + converter.startBlock(bl); + super.startBlock(bl); + } + + @Override + public void endBlock(Block bl) { + converter.endBlock(bl); + super.endBlock(bl); + } + + @Override + public void startBlockContainer(BlockContainer blc) { + converter.startBlockContainer(blc); + super.startBlockContainer(blc); + } + + @Override + public void endBlockContainer(BlockContainer blc) { + converter.endBlockContainer(blc); + super.endBlockContainer(blc); + } + + @Override + public void startInline(Inline inl) { + converter.startInline(inl); + super.startInline(inl); + } + + @Override + public void endInline(Inline inl) { + converter.endInline(inl); + super.endInline(inl); + } + + @Override + public void startTable(Table tbl) { + converter.startTable(tbl); + super.startTable(tbl); + } + + @Override + public void endTable(Table tbl) { + converter.endTable(tbl); + super.endTable(tbl); + } + + @Override + public void startColumn(TableColumn tc) { + converter.startColumn(tc); + super.startColumn(tc); + } + + @Override + public void endColumn(TableColumn tc) { + converter.endColumn(tc); + super.endColumn(tc); + } + + @Override + public void startHeader(TableHeader header) { + converter.startHeader(header); + super.startHeader(header); + } + + @Override + public void endHeader(TableHeader header) { + converter.endHeader(header); + super.endHeader(header); + } + + @Override + public void startFooter(TableFooter footer) { + converter.startFooter(footer); + super.startFooter(footer); + } + + @Override + public void endFooter(TableFooter footer) { + converter.endFooter(footer); + super.endFooter(footer); + } + + @Override + public void startBody(TableBody body) { + converter.startBody(body); + super.startBody(body); + } + + @Override + public void endBody(TableBody body) { + converter.endBody(body); + super.endBody(body); + } + + @Override + public void startRow(TableRow tr) { + converter.startRow(tr); + super.startRow(tr); + } + + @Override + public void endRow(TableRow tr) { + converter.endRow(tr); + super.endRow(tr); + } + + @Override + public void startCell(TableCell tc) { + converter.startCell(tc); + super.startCell(tc); + } + + @Override + public void endCell(TableCell tc) { + converter.endCell(tc); + super.endCell(tc); + } + + @Override + public void startList(ListBlock lb) { + converter.startList(lb); + super.startList(lb); + } + + @Override + public void endList(ListBlock lb) { + converter.endList(lb); + super.endList(lb); + } + + @Override + public void startListItem(ListItem li) { + converter.startListItem(li); + super.startListItem(li); + } + + @Override + public void endListItem(ListItem li) { + converter.endListItem(li); + super.endListItem(li); + } + + @Override + public void startListLabel(ListItemLabel listItemLabel) { + converter.startListLabel(listItemLabel); + super.startListLabel(listItemLabel); + } + + @Override + public void endListLabel(ListItemLabel listItemLabel) { + converter.endListLabel(listItemLabel); + super.endListLabel(listItemLabel); + } + + @Override + public void startListBody(ListItemBody listItemBody) { + converter.startListBody(listItemBody); + super.startListBody(listItemBody); + } + + @Override + public void endListBody(ListItemBody listItemBody) { + converter.endListBody(listItemBody); + super.endListBody(listItemBody); + } + + @Override + public void startStatic(StaticContent staticContent) { + converter.startStatic(staticContent); + super.startStatic(staticContent); + } + + @Override + public void endStatic(StaticContent statisContent) { + converter.endStatic(statisContent); + super.endStatic(statisContent); + } + + @Override + public void startMarkup() { + converter.startMarkup(); + super.startMarkup(); + } + + @Override + public void endMarkup() { + converter.endMarkup(); + super.endMarkup(); + } + + @Override + public void startLink(BasicLink basicLink) { + converter.startLink(basicLink); + super.startLink(basicLink); + } + + @Override + public void endLink(BasicLink basicLink) { + converter.endLink(basicLink); + super.endLink(basicLink); + } + + @Override + public void image(ExternalGraphic eg) { + converter.image(eg); + super.image(eg); + } + + @Override + public void pageRef() { + converter.pageRef(); + super.pageRef(); + } + + @Override + public void startInstreamForeignObject(InstreamForeignObject ifo) { + converter.startInstreamForeignObject(ifo); + super.startInstreamForeignObject(ifo); + } + + @Override + public void endInstreamForeignObject(InstreamForeignObject ifo) { + converter.endInstreamForeignObject(ifo); + super.endInstreamForeignObject(ifo); + } + + @Override + public void startFootnote(Footnote footnote) { + converter.startFootnote(footnote); + super.startFootnote(footnote); + } + + @Override + public void endFootnote(Footnote footnote) { + converter.endFootnote(footnote); + super.endFootnote(footnote); + } + + @Override + public void startFootnoteBody(FootnoteBody body) { + converter.startFootnoteBody(body); + super.startFootnoteBody(body); + } + + @Override + public void endFootnoteBody(FootnoteBody body) { + converter.endFootnoteBody(body); + super.endFootnoteBody(body); + } + + @Override + public void startLeader(Leader l) { + converter = eventSwallower; + converter.startLeader(l); + super.startLeader(l); + } + + @Override + public void endLeader(Leader l) { + converter.endLeader(l); + converter = foToStructureTreeEventAdapter; + super.endLeader(l); + } + + @Override + public void startWrapper(Wrapper wrapper) { + converter.startWrapper(wrapper); + super.startWrapper(wrapper); + } + + @Override + public void endWrapper(Wrapper wrapper) { + converter.endWrapper(wrapper); + super.endWrapper(wrapper); + } + + @Override + public void character(Character c) { + converter.character(c); + super.character(c); + } + + @Override + public void characters(char[] data, int start, int length) { + converter.characters(data, start, length); + super.characters(data, start, length); + } + + @Override + public void startExternalDocument(ExternalDocument document) { + converter.startExternalDocument(document); + super.startExternalDocument(document); + } + + @Override + public void endExternalDocument(ExternalDocument document) { + converter.endExternalDocument(document); + super.endExternalDocument(document); + } + +} diff --git a/src/java/org/apache/fop/accessibility/StructureTree.java b/src/java/org/apache/fop/accessibility/StructureTree.java deleted file mode 100644 index a8afd4ef5..000000000 --- a/src/java/org/apache/fop/accessibility/StructureTree.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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.accessibility; - -import java.io.StringWriter; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; - -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -/** - * A reduced version of the document's FO tree, containing only its logical - * structure. Used by accessible output formats. - */ -public final class StructureTree { - - private final List pageSequenceStructures = new ArrayList(); - - /** - * Package-private default constructor. - */ - public StructureTree() { } - - private static boolean flowOrStaticContentNodes(NodeList nodes) { - for (int i = 0; i < nodes.getLength(); i++) { - Node node = nodes.item(i); - if (node.getNodeType() != Node.ELEMENT_NODE) { - return false; - } - String name = node.getLocalName(); - if (!(name.equals("flow") || name.equals("static-content"))) { - return false; - } - } - return true; - } - - void addPageSequenceStructure(NodeList structureTree) { - assert flowOrStaticContentNodes(structureTree); - pageSequenceStructures.add(structureTree); - } - - /** - * Returns the list of nodes that are the children of the given page sequence. - * - * @param index index of the page sequence, 0-based - * @return its children nodes - */ - public NodeList getPageSequence(int index) { - return (NodeList) pageSequenceStructures.get(index); - } - - /** - * Returns an XML-like representation of the structure trees. - *

- * Note: use only for debugging purpose, as this method - * performs non-trivial operations. - *

- * @return a string representation of this object - */ - public String toString() { - try { - Transformer t = TransformerFactory.newInstance().newTransformer(); - Writer str = new StringWriter(); - for (Iterator iter = pageSequenceStructures.iterator(); iter.hasNext();) { - NodeList nodes = (NodeList) iter.next(); - for (int i = 0, c = nodes.getLength(); i < c; i++) { - t.transform(new DOMSource(nodes.item(i)), new StreamResult(str)); - } - } - return str.toString(); - } catch (Exception e) { - return e.toString(); - } - } - -} diff --git a/src/java/org/apache/fop/accessibility/StructureTree2SAXEventAdapter.java b/src/java/org/apache/fop/accessibility/StructureTree2SAXEventAdapter.java new file mode 100644 index 000000000..240c384d6 --- /dev/null +++ b/src/java/org/apache/fop/accessibility/StructureTree2SAXEventAdapter.java @@ -0,0 +1,104 @@ +/* + * 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.accessibility; + +import java.util.Locale; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +import org.apache.fop.fo.FOElementMapping; +import org.apache.fop.fo.extensions.ExtensionElementMapping; +import org.apache.fop.fo.extensions.InternalElementMapping; +import org.apache.fop.render.intermediate.IFConstants; + +/** + * Converts structure tree events to SAX events. + */ +public final class StructureTree2SAXEventAdapter implements StructureTreeEventHandler { + + private final ContentHandler contentHandler; + + private StructureTree2SAXEventAdapter(ContentHandler currentContentHandler) { + this.contentHandler = currentContentHandler; + } + + /** + * Factory method that creates a new instance. + * @param contentHandler The handler that receives SAX events + */ + public static StructureTreeEventHandler newInstance(ContentHandler contentHandler) { + return new StructureTree2SAXEventAdapter(contentHandler); + } + + /** {@inheritDoc} */ + public void startPageSequence(Locale locale) { + try { + + contentHandler.startPrefixMapping( + InternalElementMapping.STANDARD_PREFIX, InternalElementMapping.URI); + contentHandler.startPrefixMapping( + ExtensionElementMapping.STANDARD_PREFIX, ExtensionElementMapping.URI); + contentHandler.startElement(IFConstants.NAMESPACE, + IFConstants.EL_STRUCTURE_TREE, IFConstants.EL_STRUCTURE_TREE, + new AttributesImpl()); + } catch (SAXException e) { + throw new RuntimeException(e); + } + } + + /** {@inheritDoc} */ + public void endPageSequence() { + try { + contentHandler.endElement(IFConstants.NAMESPACE, IFConstants.EL_STRUCTURE_TREE, + IFConstants.EL_STRUCTURE_TREE); + contentHandler.endPrefixMapping( + ExtensionElementMapping.STANDARD_PREFIX); + contentHandler.endPrefixMapping( + InternalElementMapping.STANDARD_PREFIX); + + } catch (SAXException e) { + throw new RuntimeException(e); + } + } + + /** {@inheritDoc} */ + public void startNode(String name, Attributes attributes) { + try { + contentHandler.startElement(FOElementMapping.URI, name, + FOElementMapping.STANDARD_PREFIX + ":" + name, + attributes); + } catch (SAXException e) { + throw new RuntimeException(e); + } + } + + /** {@inheritDoc} */ + public void endNode(String name) { + try { + contentHandler.endElement(FOElementMapping.URI, name, + FOElementMapping.STANDARD_PREFIX + ":" + name); + } catch (SAXException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/java/org/apache/fop/accessibility/StructureTreeBuilder.java b/src/java/org/apache/fop/accessibility/StructureTreeBuilder.java deleted file mode 100644 index 036502e99..000000000 --- a/src/java/org/apache/fop/accessibility/StructureTreeBuilder.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.accessibility; - -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.dom.DOMResult; -import javax.xml.transform.sax.SAXTransformerFactory; -import javax.xml.transform.sax.TransformerHandler; - -import org.xml.sax.ContentHandler; -import org.xml.sax.SAXException; - -import org.apache.fop.util.DelegatingContentHandler; - -/** - * Helper class that re-builds a structure tree from what is stored in an - * intermediate XML file (IF XML or Area Tree XML). - */ -public final class StructureTreeBuilder { - - private final SAXTransformerFactory factory; - - private final StructureTree structureTree = new StructureTree(); - - /** - * Creates a new instance. - * - * @param factory a factory internally used to build the structures of page - * sequences - */ - public StructureTreeBuilder(SAXTransformerFactory factory) { - this.factory = factory; - } - - /** - * Returns the structure tree that will result from the parsing. - * - * @return the structure tree built by this object - */ - public StructureTree getStructureTree() { - return structureTree; - } - - /** - * Returns a ContenHandler for parsing the structure of a new page sequence. - * It is assumed that page sequences are being parsed in the document order. - * - * @return a handler for parsing the <structure-tree> or - * <structureTree> element and its descendants - * @throws SAXException if there is an error when creating the handler - */ - public ContentHandler getHandlerForNextPageSequence() throws SAXException { - TransformerHandler structureTreeBuilder; - try { - structureTreeBuilder = factory.newTransformerHandler(); - } catch (TransformerConfigurationException e) { - throw new SAXException(e); - } - final DOMResult domResult = new DOMResult(); - structureTreeBuilder.setResult(domResult); - return new DelegatingContentHandler(structureTreeBuilder) { - - public void characters(char[] ch, int start, int length) throws SAXException { - /* - * There's no text node in the structure tree. This is just - * whitespace => ignore - */ - } - - public void endDocument() throws SAXException { - super.endDocument(); - structureTree.addPageSequenceStructure(domResult.getNode().getFirstChild() - .getChildNodes()); - } - }; - } - -} diff --git a/src/java/org/apache/fop/accessibility/StructureTreeBuildingFOEventHandler.java b/src/java/org/apache/fop/accessibility/StructureTreeBuildingFOEventHandler.java deleted file mode 100644 index e3f946810..000000000 --- a/src/java/org/apache/fop/accessibility/StructureTreeBuildingFOEventHandler.java +++ /dev/null @@ -1,840 +0,0 @@ -/* - * 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.accessibility; - -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMResult; -import javax.xml.transform.sax.SAXTransformerFactory; -import javax.xml.transform.sax.TransformerHandler; - -import org.xml.sax.SAXException; -import org.xml.sax.helpers.AttributesImpl; - -import org.apache.fop.apps.FOUserAgent; -import org.apache.fop.fo.DelegatingFOEventHandler; -import org.apache.fop.fo.FOEventHandler; -import org.apache.fop.fo.FONode; -import org.apache.fop.fo.extensions.ExtensionElementMapping; -import org.apache.fop.fo.extensions.ExternalDocument; -import org.apache.fop.fo.extensions.InternalElementMapping; -import org.apache.fop.fo.flow.AbstractGraphics; -import org.apache.fop.fo.flow.BasicLink; -import org.apache.fop.fo.flow.Block; -import org.apache.fop.fo.flow.BlockContainer; -import org.apache.fop.fo.flow.Character; -import org.apache.fop.fo.flow.ExternalGraphic; -import org.apache.fop.fo.flow.Footnote; -import org.apache.fop.fo.flow.FootnoteBody; -import org.apache.fop.fo.flow.Inline; -import org.apache.fop.fo.flow.InstreamForeignObject; -import org.apache.fop.fo.flow.Leader; -import org.apache.fop.fo.flow.ListBlock; -import org.apache.fop.fo.flow.ListItem; -import org.apache.fop.fo.flow.ListItemBody; -import org.apache.fop.fo.flow.ListItemLabel; -import org.apache.fop.fo.flow.PageNumber; -import org.apache.fop.fo.flow.PageNumberCitation; -import org.apache.fop.fo.flow.PageNumberCitationLast; -import org.apache.fop.fo.flow.Wrapper; -import org.apache.fop.fo.flow.table.Table; -import org.apache.fop.fo.flow.table.TableBody; -import org.apache.fop.fo.flow.table.TableCell; -import org.apache.fop.fo.flow.table.TableColumn; -import org.apache.fop.fo.flow.table.TableFooter; -import org.apache.fop.fo.flow.table.TableHeader; -import org.apache.fop.fo.flow.table.TableRow; -import org.apache.fop.fo.pagination.Flow; -import org.apache.fop.fo.pagination.PageSequence; -import org.apache.fop.fo.pagination.StaticContent; -import org.apache.fop.fo.properties.CommonAccessibilityHolder; -import org.apache.fop.util.XMLUtil; - -/** - * A class that builds the document's structure tree. - */ -public class StructureTreeBuildingFOEventHandler extends DelegatingFOEventHandler { - - private int idCounter; - - private final StructureTree structureTree; - - private TransformerHandler structureTreeDOMBuilder; - - private DOMResult result; - - /** Delegates to either {@link #actualStructureTreeBuilder} or {@link #eventSwallower}. */ - private FOEventHandler structureTreeBuilder; - - private FOEventHandler actualStructureTreeBuilder; - - /** The descendants of some elements like fo:leader must be ignored. */ - private final FOEventHandler eventSwallower; - - private final class StructureTreeBuilder extends FOEventHandler { - - public StructureTreeBuilder(FOUserAgent foUserAgent) { - super(foUserAgent); - } - - @Override - public void startDocument() throws SAXException { - } - - @Override - public void endDocument() throws SAXException { - } - - @Override - public void startPageSequence(PageSequence pageSeq) { - SAXTransformerFactory transformerFactory = - (SAXTransformerFactory) TransformerFactory.newInstance(); - try { - structureTreeDOMBuilder = transformerFactory.newTransformerHandler(); - } catch (TransformerConfigurationException e) { - throw new RuntimeException(e); - } - result = new DOMResult(); - structureTreeDOMBuilder.setResult(result); - try { - structureTreeDOMBuilder.startDocument(); - } catch (SAXException e) { - throw new RuntimeException(e); - } - startElement(pageSeq); - } - - @Override - public void endPageSequence(PageSequence pageSeq) { - endElement(pageSeq); - try { - structureTreeDOMBuilder.endDocument(); - } catch (SAXException e) { - throw new RuntimeException(e); - } - structureTree.addPageSequenceStructure( - result.getNode().getFirstChild().getChildNodes()); - } - - @Override - public void startPageNumber(PageNumber pagenum) { - startElementWithID(pagenum); - } - - @Override - public void endPageNumber(PageNumber pagenum) { - endElement(pagenum); - } - - @Override - public void startPageNumberCitation(PageNumberCitation pageCite) { - startElementWithID(pageCite); - } - - @Override - public void endPageNumberCitation(PageNumberCitation pageCite) { - endElement(pageCite); - } - - @Override - public void startPageNumberCitationLast(PageNumberCitationLast pageLast) { - startElementWithID(pageLast); - } - - @Override - public void endPageNumberCitationLast(PageNumberCitationLast pageLast) { - endElement(pageLast); - } - - @Override - public void startFlow(Flow fl) { - startElement(fl); - } - - @Override - public void endFlow(Flow fl) { - endElement(fl); - } - - @Override - public void startBlock(Block bl) { - startElementWithID(bl); - } - - @Override - public void endBlock(Block bl) { - endElement(bl); - } - - @Override - public void startBlockContainer(BlockContainer blc) { - startElement(blc); - } - - @Override - public void endBlockContainer(BlockContainer blc) { - endElement(blc); - } - - @Override - public void startInline(Inline inl) { - startElementWithID(inl); - } - - @Override - public void endInline(Inline inl) { - endElement(inl); - } - - @Override - public void startTable(Table tbl) { - startElementWithID(tbl); - } - - @Override - public void endTable(Table tbl) { - endElement(tbl); - } - - @Override - public void startHeader(TableHeader header) { - startElementWithID(header); - } - - @Override - public void endHeader(TableHeader header) { - endElement(header); - } - - @Override - public void startFooter(TableFooter footer) { - startElementWithID(footer); - } - - @Override - public void endFooter(TableFooter footer) { - endElement(footer); - } - - @Override - public void startBody(TableBody body) { - startElementWithID(body); - } - - @Override - public void endBody(TableBody body) { - endElement(body); - } - - @Override - public void startRow(TableRow tr) { - startElementWithID(tr); - } - - @Override - public void endRow(TableRow tr) { - endElement(tr); - } - - @Override - public void startCell(TableCell tc) { - AttributesImpl attributes = new AttributesImpl(); - int colSpan = tc.getNumberColumnsSpanned(); - if (colSpan > 1) { - addNoNamespaceAttribute(attributes, "number-columns-spanned", - Integer.toString(colSpan)); - } - startElementWithID(tc, attributes); - } - - @Override - public void endCell(TableCell tc) { - endElement(tc); - } - - @Override - public void startList(ListBlock lb) { - startElement(lb); - } - - @Override - public void endList(ListBlock lb) { - endElement(lb); - } - - @Override - public void startListItem(ListItem li) { - startElement(li); - } - - @Override - public void endListItem(ListItem li) { - endElement(li); - } - - @Override - public void startListLabel(ListItemLabel listItemLabel) { - startElement(listItemLabel); - } - - @Override - public void endListLabel(ListItemLabel listItemLabel) { - endElement(listItemLabel); - } - - @Override - public void startListBody(ListItemBody listItemBody) { - startElement(listItemBody); - } - - @Override - public void endListBody(ListItemBody listItemBody) { - endElement(listItemBody); - } - - @Override - public void startStatic(StaticContent staticContent) { - startElement(staticContent); - } - - @Override - public void endStatic(StaticContent statisContent) { - endElement(statisContent); - } - - @Override - public void startLink(BasicLink basicLink) { - startElementWithID(basicLink); - } - - @Override - public void endLink(BasicLink basicLink) { - endElement(basicLink); - } - - @Override - public void image(ExternalGraphic eg) { - startElementWithIDAndAltText(eg); - endElement(eg); - } - - @Override - public void startInstreamForeignObject(InstreamForeignObject ifo) { - startElementWithIDAndAltText(ifo); - } - - @Override - public void endInstreamForeignObject(InstreamForeignObject ifo) { - endElement(ifo); - } - - @Override - public void startFootnote(Footnote footnote) { - startElement(footnote); - } - - @Override - public void endFootnote(Footnote footnote) { - endElement(footnote); - } - - @Override - public void startFootnoteBody(FootnoteBody body) { - startElement(body); - } - - @Override - public void endFootnoteBody(FootnoteBody body) { - endElement(body); - } - - @Override - public void startWrapper(Wrapper wrapper) { - startElement(wrapper); - } - - @Override - public void endWrapper(Wrapper wrapper) { - endElement(wrapper); - } - - @Override - public void character(Character c) { - startElementWithID(c); - endElement(c); - } - - private void startElement(FONode node) { - startElement(node, new AttributesImpl()); - } - - private void startElementWithID(FONode node) { - startElementWithID(node, new AttributesImpl()); - } - - private void startElementWithIDAndAltText(AbstractGraphics node) { - AttributesImpl attributes = new AttributesImpl(); - addAttribute(attributes, ExtensionElementMapping.URI, "alt-text", - ExtensionElementMapping.STANDARD_PREFIX, node.getAltText()); - startElementWithID(node, attributes); - } - - private void startElementWithID(FONode node, AttributesImpl attributes) { - String id = Integer.toHexString(idCounter++); - node.setPtr(id); - addAttribute(attributes, - InternalElementMapping.URI, "ptr", InternalElementMapping.STANDARD_PREFIX, id); - startElement(node, attributes); - } - - private void startElement(FONode node, AttributesImpl attributes) { - String localName = node.getLocalName(); - if (node instanceof CommonAccessibilityHolder) { - addRole((CommonAccessibilityHolder) node, attributes); - } - try { - structureTreeDOMBuilder.startElement(node.getNamespaceURI(), localName, - node.getNormalNamespacePrefix() + ":" + localName, - attributes); - } catch (SAXException e) { - throw new RuntimeException(e); - } - } - - private void addNoNamespaceAttribute(AttributesImpl attributes, String name, String value) { - attributes.addAttribute("", name, name, XMLUtil.CDATA, value); - } - - private void addAttribute(AttributesImpl attributes, - String namespace, String localName, String prefix, String value) { - assert namespace.length() > 0 && prefix.length() > 0; - String qualifiedName = prefix + ":" + localName; - attributes.addAttribute(namespace, localName, qualifiedName, XMLUtil.CDATA, value); - } - - private void addRole(CommonAccessibilityHolder node, AttributesImpl attributes) { - String role = node.getCommonAccessibility().getRole(); - if (role != null) { - addNoNamespaceAttribute(attributes, "role", role); - } - } - - private void endElement(FONode node) { - String localName = node.getLocalName(); - try { - structureTreeDOMBuilder.endElement(node.getNamespaceURI(), localName, - node.getNormalNamespacePrefix() + ":" + localName); - } catch (SAXException e) { - throw new RuntimeException(e); - } - } - - } - - /** - * Creates a new instance. - * - * @param structureTree the object that will hold the structure tree - * @param delegate the FO event handler that must be wrapped by this instance - */ - public StructureTreeBuildingFOEventHandler(StructureTree structureTree, - FOEventHandler delegate) { - super(delegate); - this.structureTree = structureTree; - this.actualStructureTreeBuilder = new StructureTreeBuilder(foUserAgent); - this.structureTreeBuilder = actualStructureTreeBuilder; - this.eventSwallower = new FOEventHandler(foUserAgent) { }; - } - - @Override - public void startDocument() throws SAXException { - structureTreeBuilder.startDocument(); - super.startDocument(); - } - - @Override - public void endDocument() throws SAXException { - structureTreeBuilder.endDocument(); - super.endDocument(); - } - - @Override - public void startPageSequence(PageSequence pageSeq) { - structureTreeBuilder.startPageSequence(pageSeq); - super.startPageSequence(pageSeq); - } - - @Override - public void endPageSequence(PageSequence pageSeq) { - structureTreeBuilder.endPageSequence(pageSeq); - super.endPageSequence(pageSeq); - } - - @Override - public void startPageNumber(PageNumber pagenum) { - structureTreeBuilder.startPageNumber(pagenum); - super.startPageNumber(pagenum); - } - - @Override - public void endPageNumber(PageNumber pagenum) { - structureTreeBuilder.endPageNumber(pagenum); - super.endPageNumber(pagenum); - } - - @Override - public void startPageNumberCitation(PageNumberCitation pageCite) { - structureTreeBuilder.startPageNumberCitation(pageCite); - super.startPageNumberCitation(pageCite); - } - - @Override - public void endPageNumberCitation(PageNumberCitation pageCite) { - structureTreeBuilder.endPageNumberCitation(pageCite); - super.endPageNumberCitation(pageCite); - } - - @Override - public void startPageNumberCitationLast(PageNumberCitationLast pageLast) { - structureTreeBuilder.startPageNumberCitationLast(pageLast); - super.startPageNumberCitationLast(pageLast); - } - - @Override - public void endPageNumberCitationLast(PageNumberCitationLast pageLast) { - structureTreeBuilder.endPageNumberCitationLast(pageLast); - super.endPageNumberCitationLast(pageLast); - } - - @Override - public void startFlow(Flow fl) { - structureTreeBuilder.startFlow(fl); - super.startFlow(fl); - } - - @Override - public void endFlow(Flow fl) { - structureTreeBuilder.endFlow(fl); - super.endFlow(fl); - } - - @Override - public void startBlock(Block bl) { - structureTreeBuilder.startBlock(bl); - super.startBlock(bl); - } - - @Override - public void endBlock(Block bl) { - structureTreeBuilder.endBlock(bl); - super.endBlock(bl); - } - - @Override - public void startBlockContainer(BlockContainer blc) { - structureTreeBuilder.startBlockContainer(blc); - super.startBlockContainer(blc); - } - - @Override - public void endBlockContainer(BlockContainer blc) { - structureTreeBuilder.endBlockContainer(blc); - super.endBlockContainer(blc); - } - - @Override - public void startInline(Inline inl) { - structureTreeBuilder.startInline(inl); - super.startInline(inl); - } - - @Override - public void endInline(Inline inl) { - structureTreeBuilder.endInline(inl); - super.endInline(inl); - } - - @Override - public void startTable(Table tbl) { - structureTreeBuilder.startTable(tbl); - super.startTable(tbl); - } - - @Override - public void endTable(Table tbl) { - structureTreeBuilder.endTable(tbl); - super.endTable(tbl); - } - - @Override - public void startColumn(TableColumn tc) { - structureTreeBuilder.startColumn(tc); - super.startColumn(tc); - } - - @Override - public void endColumn(TableColumn tc) { - structureTreeBuilder.endColumn(tc); - super.endColumn(tc); - } - - @Override - public void startHeader(TableHeader header) { - structureTreeBuilder.startHeader(header); - super.startHeader(header); - } - - @Override - public void endHeader(TableHeader header) { - structureTreeBuilder.endHeader(header); - super.endHeader(header); - } - - @Override - public void startFooter(TableFooter footer) { - structureTreeBuilder.startFooter(footer); - super.startFooter(footer); - } - - @Override - public void endFooter(TableFooter footer) { - structureTreeBuilder.endFooter(footer); - super.endFooter(footer); - } - - @Override - public void startBody(TableBody body) { - structureTreeBuilder.startBody(body); - super.startBody(body); - } - - @Override - public void endBody(TableBody body) { - structureTreeBuilder.endBody(body); - super.endBody(body); - } - - @Override - public void startRow(TableRow tr) { - structureTreeBuilder.startRow(tr); - super.startRow(tr); - } - - @Override - public void endRow(TableRow tr) { - structureTreeBuilder.endRow(tr); - super.endRow(tr); - } - - @Override - public void startCell(TableCell tc) { - structureTreeBuilder.startCell(tc); - super.startCell(tc); - } - - @Override - public void endCell(TableCell tc) { - structureTreeBuilder.endCell(tc); - super.endCell(tc); - } - - @Override - public void startList(ListBlock lb) { - structureTreeBuilder.startList(lb); - super.startList(lb); - } - - @Override - public void endList(ListBlock lb) { - structureTreeBuilder.endList(lb); - super.endList(lb); - } - - @Override - public void startListItem(ListItem li) { - structureTreeBuilder.startListItem(li); - super.startListItem(li); - } - - @Override - public void endListItem(ListItem li) { - structureTreeBuilder.endListItem(li); - super.endListItem(li); - } - - @Override - public void startListLabel(ListItemLabel listItemLabel) { - structureTreeBuilder.startListLabel(listItemLabel); - super.startListLabel(listItemLabel); - } - - @Override - public void endListLabel(ListItemLabel listItemLabel) { - structureTreeBuilder.endListLabel(listItemLabel); - super.endListLabel(listItemLabel); - } - - @Override - public void startListBody(ListItemBody listItemBody) { - structureTreeBuilder.startListBody(listItemBody); - super.startListBody(listItemBody); - } - - @Override - public void endListBody(ListItemBody listItemBody) { - structureTreeBuilder.endListBody(listItemBody); - super.endListBody(listItemBody); - } - - @Override - public void startStatic(StaticContent staticContent) { - structureTreeBuilder.startStatic(staticContent); - super.startStatic(staticContent); - } - - @Override - public void endStatic(StaticContent statisContent) { - structureTreeBuilder.endStatic(statisContent); - super.endStatic(statisContent); - } - - @Override - public void startMarkup() { - structureTreeBuilder.startMarkup(); - super.startMarkup(); - } - - @Override - public void endMarkup() { - structureTreeBuilder.endMarkup(); - super.endMarkup(); - } - - @Override - public void startLink(BasicLink basicLink) { - structureTreeBuilder.startLink(basicLink); - super.startLink(basicLink); - } - - @Override - public void endLink(BasicLink basicLink) { - structureTreeBuilder.endLink(basicLink); - super.endLink(basicLink); - } - - @Override - public void image(ExternalGraphic eg) { - structureTreeBuilder.image(eg); - super.image(eg); - } - - @Override - public void pageRef() { - structureTreeBuilder.pageRef(); - super.pageRef(); - } - - @Override - public void startInstreamForeignObject(InstreamForeignObject ifo) { - structureTreeBuilder.startInstreamForeignObject(ifo); - super.startInstreamForeignObject(ifo); - } - - @Override - public void endInstreamForeignObject(InstreamForeignObject ifo) { - structureTreeBuilder.endInstreamForeignObject(ifo); - super.endInstreamForeignObject(ifo); - } - - @Override - public void startFootnote(Footnote footnote) { - structureTreeBuilder.startFootnote(footnote); - super.startFootnote(footnote); - } - - @Override - public void endFootnote(Footnote footnote) { - structureTreeBuilder.endFootnote(footnote); - super.endFootnote(footnote); - } - - @Override - public void startFootnoteBody(FootnoteBody body) { - structureTreeBuilder.startFootnoteBody(body); - super.startFootnoteBody(body); - } - - @Override - public void endFootnoteBody(FootnoteBody body) { - structureTreeBuilder.endFootnoteBody(body); - super.endFootnoteBody(body); - } - - @Override - public void startLeader(Leader l) { - structureTreeBuilder = eventSwallower; - structureTreeBuilder.startLeader(l); - super.startLeader(l); - } - - @Override - public void endLeader(Leader l) { - structureTreeBuilder.endLeader(l); - structureTreeBuilder = actualStructureTreeBuilder; - super.endLeader(l); - } - - @Override - public void startWrapper(Wrapper wrapper) { - structureTreeBuilder.startWrapper(wrapper); - super.startWrapper(wrapper); - } - - @Override - public void endWrapper(Wrapper wrapper) { - structureTreeBuilder.endWrapper(wrapper); - super.endWrapper(wrapper); - } - - @Override - public void character(Character c) { - structureTreeBuilder.character(c); - super.character(c); - } - - @Override - public void characters(char[] data, int start, int length) { - structureTreeBuilder.characters(data, start, length); - super.characters(data, start, length); - } - - @Override - public void startExternalDocument(ExternalDocument document) { - structureTreeBuilder.startExternalDocument(document); - super.startExternalDocument(document); - } - - @Override - public void endExternalDocument(ExternalDocument document) { - structureTreeBuilder.endExternalDocument(document); - super.endExternalDocument(document); - } - -} diff --git a/src/java/org/apache/fop/accessibility/StructureTreeEventHandler.java b/src/java/org/apache/fop/accessibility/StructureTreeEventHandler.java new file mode 100644 index 000000000..d84d07870 --- /dev/null +++ b/src/java/org/apache/fop/accessibility/StructureTreeEventHandler.java @@ -0,0 +1,56 @@ +/* + * 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.accessibility; + +import java.util.Locale; + +import org.xml.sax.Attributes; + +/** + * Receive notifications relating to the structure tree of an FO document. + * A structure tree is a reduced version of the document's FO tree, containing only the logical + * structure that is used by accessible output formats. + */ +public interface StructureTreeEventHandler { + + /** + * Starts a page sequence structure tree node. + * @param locale The locale of the page sequence + */ + void startPageSequence(Locale locale); + + /** + * Starts a structure tree node. + * @param name The name of the structure tree node + * @param attributes Map of node properties + */ + void startNode(String name, Attributes attributes); + + /** + * Ends a structure tree node. + * @param name The name of the structure tree node + */ + void endNode(String name); + + /** + * Ends a page sequence structure tree node. + */ + void endPageSequence(); +} diff --git a/src/java/org/apache/fop/apps/FOUserAgent.java b/src/java/org/apache/fop/apps/FOUserAgent.java index 071066e04..d5cc695c8 100644 --- a/src/java/org/apache/fop/apps/FOUserAgent.java +++ b/src/java/org/apache/fop/apps/FOUserAgent.java @@ -39,7 +39,8 @@ import org.apache.xmlgraphics.util.UnitConv; import org.apache.fop.Version; import org.apache.fop.accessibility.Accessibility; -import org.apache.fop.accessibility.StructureTree; +import org.apache.fop.accessibility.DummyStructureTreeEventHandler; +import org.apache.fop.accessibility.StructureTreeEventHandler; import org.apache.fop.events.DefaultEventBroadcaster; import org.apache.fop.events.Event; import org.apache.fop.events.EventBroadcaster; @@ -101,8 +102,7 @@ public class FOUserAgent { private boolean locatorEnabled = true; // true by default (for error messages). private boolean conserveMemoryPolicy = false; private EventBroadcaster eventBroadcaster = new FOPEventBroadcaster(); - - private StructureTree structureTree; + private StructureTreeEventHandler structureTreeEventHandler = DummyStructureTreeEventHandler.INSTANCE; /** Producer: Metadata element for the system/software that produces * the document. (Some renderers can store this in the document.) @@ -173,6 +173,9 @@ public class FOUserAgent { * @param documentHandler the document handler instance to use */ public void setDocumentHandlerOverride(IFDocumentHandler documentHandler) { + if (isAccessibilityEnabled()) { + setStructureTreeEventHandler(documentHandler.getStructureTreeEventHandler()); + } this.documentHandlerOverride = documentHandler; } @@ -674,24 +677,23 @@ public class FOUserAgent { } /** - * Sets the document's structure tree, for use by accessible output formats. + * Sets the document's structure tree event handler, for use by accessible + * output formats. * - * @param structureTree a simplified version of the FO tree, retaining only - * its logical structure + * @param structureTreeEventHandler The structure tree event handler to set */ - public void setStructureTree(StructureTree structureTree) { - this.structureTree = structureTree; + public void setStructureTreeEventHandler(StructureTreeEventHandler structureTreeEventHandler) { + this.structureTreeEventHandler = structureTreeEventHandler; } /** - * Returns the document's structure tree, for use by accessible output - * formats. + * Returns the document's structure tree event handler, for use by + * accessible output formats. * - * @return a simplified version of the FO tree, retaining only its logical - * structure + * @return The structure tree event handler */ - public StructureTree getStructureTree() { - return this.structureTree; + public StructureTreeEventHandler getStructureTreeEventHandler() { + return this.structureTreeEventHandler; } } diff --git a/src/java/org/apache/fop/area/AreaTreeHandler.java b/src/java/org/apache/fop/area/AreaTreeHandler.java index f45245bfe..6448e7c5d 100644 --- a/src/java/org/apache/fop/area/AreaTreeHandler.java +++ b/src/java/org/apache/fop/area/AreaTreeHandler.java @@ -22,6 +22,7 @@ package org.apache.fop.area; // Java import java.io.OutputStream; import java.util.List; +import java.util.Locale; import org.xml.sax.SAXException; @@ -182,6 +183,14 @@ public class AreaTreeHandler extends FOEventHandler { } } + @Override + public void startRoot(Root root) { + Locale locale = root.getLocale(); + if (locale != null) { + model.setDocumentLocale(locale); + } + } + /** * finish the previous pageSequence */ diff --git a/src/java/org/apache/fop/area/AreaTreeModel.java b/src/java/org/apache/fop/area/AreaTreeModel.java index 2a1f14ab5..941f6cea2 100644 --- a/src/java/org/apache/fop/area/AreaTreeModel.java +++ b/src/java/org/apache/fop/area/AreaTreeModel.java @@ -21,6 +21,7 @@ package org.apache.fop.area; // Java import java.util.List; +import java.util.Locale; import org.xml.sax.SAXException; @@ -123,4 +124,11 @@ public class AreaTreeModel { public PageViewport getPage(int seq, int count) { return pageSequenceList.get(seq - 1).getPage(count); } + + /** + * + * @param locale The locale of the document + */ + public void setDocumentLocale(Locale locale) { + } } diff --git a/src/java/org/apache/fop/area/AreaTreeParser.java b/src/java/org/apache/fop/area/AreaTreeParser.java index d2f52f991..545c9015d 100644 --- a/src/java/org/apache/fop/area/AreaTreeParser.java +++ b/src/java/org/apache/fop/area/AreaTreeParser.java @@ -58,8 +58,6 @@ import org.apache.xmlgraphics.image.loader.ImageSessionContext; import org.apache.xmlgraphics.util.QName; import org.apache.fop.ResourceEventProducer; -import org.apache.fop.accessibility.AccessibilityEventProducer; -import org.apache.fop.accessibility.StructureTreeBuilder; import org.apache.fop.apps.FOUserAgent; import org.apache.fop.area.Trait.Background; import org.apache.fop.area.Trait.InternalLink; @@ -69,11 +67,11 @@ import org.apache.fop.area.inline.Image; import org.apache.fop.area.inline.InlineArea; import org.apache.fop.area.inline.InlineBlockParent; import org.apache.fop.area.inline.InlineParent; +import org.apache.fop.area.inline.InlineViewport; import org.apache.fop.area.inline.Leader; import org.apache.fop.area.inline.Space; import org.apache.fop.area.inline.SpaceArea; import org.apache.fop.area.inline.TextArea; -import org.apache.fop.area.inline.InlineViewport; import org.apache.fop.area.inline.WordArea; import org.apache.fop.fo.ElementMappingRegistry; import org.apache.fop.fo.expr.PropertyException; @@ -86,7 +84,6 @@ import org.apache.fop.util.ContentHandlerFactory; import org.apache.fop.util.ContentHandlerFactoryRegistry; import org.apache.fop.util.ConversionUtils; import org.apache.fop.util.DefaultErrorListener; -import org.apache.fop.util.DelegatingContentHandler; import org.apache.fop.util.XMLConstants; import org.apache.fop.util.XMLUtil; @@ -166,27 +163,8 @@ public class AreaTreeParser { private DOMImplementation domImplementation; private Locator locator; - - private StructureTreeBuilder structureTreeBuilder; - - private ContentHandler structureTreeBuilderWrapper; - private Attributes pageSequenceAttributes; - private final class StructureTreeBuilderWrapper extends DelegatingContentHandler { - - private StructureTreeBuilderWrapper() - throws SAXException { - super(structureTreeBuilder.getHandlerForNextPageSequence()); - } - - public void endDocument() throws SAXException { - super.endDocument(); - startAreaTreeElement("pageSequence", pageSequenceAttributes); - pageSequenceAttributes = null; - } - } - public Handler(AreaTreeModel treeModel, FOUserAgent userAgent, ElementMappingRegistry elementMappingRegistry) { this.treeModel = treeModel; @@ -223,11 +201,6 @@ public class AreaTreeParser { makers.put("bookmarkTree", new BookmarkTreeMaker()); makers.put("bookmark", new BookmarkMaker()); makers.put("destination", new DestinationMaker()); - - if (userAgent.isAccessibilityEnabled()) { - structureTreeBuilder = new StructureTreeBuilder(tFactory); - userAgent.setStructureTree(structureTreeBuilder.getStructureTree()); - } } private Area findAreaType(Class clazz) { @@ -308,32 +281,15 @@ public class AreaTreeParser { } else { boolean handled = true; if ("".equals(uri)) { - if (localName.equals("pageSequence") && userAgent.isAccessibilityEnabled()) { - structureTreeBuilderWrapper = new StructureTreeBuilderWrapper(); - pageSequenceAttributes = new AttributesImpl(attributes); - } else if (localName.equals("structureTree")) { - if (userAgent.isAccessibilityEnabled()) { - delegate = structureTreeBuilderWrapper; - } else { - /* Delegate to a handler that does nothing */ - delegate = new DefaultHandler(); - } + if (localName.equals("structureTree")) { + + /* The area tree parser no longer supports the structure tree. */ + delegate = new DefaultHandler(); + delegateStack.push(qName); delegate.startDocument(); delegate.startElement(uri, localName, qName, attributes); } else { - if (pageSequenceAttributes != null) { - /* - * This means that no structure-element tag was - * found in the XML, otherwise a - * StructureTreeBuilderWrapper object would have - * been created, which would have reset the - * pageSequenceAttributes field. - */ - AccessibilityEventProducer.Provider - .get(userAgent.getEventBroadcaster()) - .noStructureTreeInXML(this); - } handled = startAreaTreeElement(localName, attributes); } } else { diff --git a/src/java/org/apache/fop/area/RenderPagesModel.java b/src/java/org/apache/fop/area/RenderPagesModel.java index afec850f8..cef2552c8 100644 --- a/src/java/org/apache/fop/area/RenderPagesModel.java +++ b/src/java/org/apache/fop/area/RenderPagesModel.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.OutputStream; import java.util.Iterator; import java.util.List; +import java.util.Locale; import org.xml.sax.SAXException; @@ -83,6 +84,11 @@ public class RenderPagesModel extends AreaTreeModel { } } + @Override + public void setDocumentLocale(Locale locale) { + renderer.setDocumentLocale(locale); + } + /** {@inheritDoc} */ @Override public void startPageSequence(PageSequence pageSequence) { diff --git a/src/java/org/apache/fop/fo/DelegatingFOEventHandler.java b/src/java/org/apache/fop/fo/DelegatingFOEventHandler.java index cb4ded17f..ed85bd1c9 100644 --- a/src/java/org/apache/fop/fo/DelegatingFOEventHandler.java +++ b/src/java/org/apache/fop/fo/DelegatingFOEventHandler.java @@ -50,6 +50,7 @@ import org.apache.fop.fo.flow.table.TableHeader; import org.apache.fop.fo.flow.table.TableRow; import org.apache.fop.fo.pagination.Flow; import org.apache.fop.fo.pagination.PageSequence; +import org.apache.fop.fo.pagination.Root; import org.apache.fop.fo.pagination.StaticContent; import org.apache.fop.fonts.FontInfo; @@ -90,6 +91,16 @@ public abstract class DelegatingFOEventHandler extends FOEventHandler { delegate.endDocument(); } + @Override + public void startRoot(Root root) { + delegate.startRoot(root); + } + + @Override + public void endRoot(Root root) { + delegate.endRoot(root); + } + @Override public void startPageSequence(PageSequence pageSeq) { delegate.startPageSequence(pageSeq); diff --git a/src/java/org/apache/fop/fo/FOElementMapping.java b/src/java/org/apache/fop/fo/FOElementMapping.java index 8069880c6..c96c3b933 100644 --- a/src/java/org/apache/fop/fo/FOElementMapping.java +++ b/src/java/org/apache/fop/fo/FOElementMapping.java @@ -32,6 +32,9 @@ public class FOElementMapping extends ElementMapping { /** The XSL-FO namespace URI */ public static final String URI = "http://www.w3.org/1999/XSL/Format"; + /** Standard prefix */ + public static final String STANDARD_PREFIX = "fo"; + /** * Basic constructor; inititializes the namespace URI for the fo: namespace */ @@ -141,7 +144,7 @@ public class FOElementMapping extends ElementMapping { /** {@inheritDoc} */ public String getStandardPrefix() { - return "fo"; + return STANDARD_PREFIX; } /** {@inheritDoc} */ diff --git a/src/java/org/apache/fop/fo/FOEventHandler.java b/src/java/org/apache/fop/fo/FOEventHandler.java index 453edec35..1f3514dea 100644 --- a/src/java/org/apache/fop/fo/FOEventHandler.java +++ b/src/java/org/apache/fop/fo/FOEventHandler.java @@ -50,6 +50,7 @@ import org.apache.fop.fo.flow.table.TableHeader; import org.apache.fop.fo.flow.table.TableRow; import org.apache.fop.fo.pagination.Flow; import org.apache.fop.fo.pagination.PageSequence; +import org.apache.fop.fo.pagination.Root; import org.apache.fop.fo.pagination.StaticContent; import org.apache.fop.fonts.FontEventAdapter; import org.apache.fop.fonts.FontInfo; @@ -117,6 +118,12 @@ public abstract class FOEventHandler { public void endDocument() throws SAXException { } + public void startRoot(Root root) { + } + + public void endRoot(Root root) { + } + /** * * @param pageSeq PageSequence that is starting. diff --git a/src/java/org/apache/fop/fo/FOTreeBuilder.java b/src/java/org/apache/fop/fo/FOTreeBuilder.java index 890138e5c..b053692d6 100644 --- a/src/java/org/apache/fop/fo/FOTreeBuilder.java +++ b/src/java/org/apache/fop/fo/FOTreeBuilder.java @@ -33,8 +33,7 @@ import org.apache.commons.logging.LogFactory; import org.apache.xmlgraphics.util.QName; -import org.apache.fop.accessibility.StructureTree; -import org.apache.fop.accessibility.StructureTreeBuildingFOEventHandler; +import org.apache.fop.accessibility.FO2StructureTreeConverter; import org.apache.fop.apps.FOPException; import org.apache.fop.apps.FOUserAgent; import org.apache.fop.apps.FormattingResults; @@ -107,9 +106,8 @@ public class FOTreeBuilder extends DefaultHandler { foEventHandler = foUserAgent.getRendererFactory().createFOEventHandler( foUserAgent, outputFormat, stream); if (userAgent.isAccessibilityEnabled()) { - StructureTree structureTree = new StructureTree(); - foEventHandler = new StructureTreeBuildingFOEventHandler(structureTree, foEventHandler); - userAgent.setStructureTree(structureTree); + foEventHandler = new FO2StructureTreeConverter( + foUserAgent.getStructureTreeEventHandler(), foEventHandler); } builderContext = new FOTreeBuilderContext(); builderContext.setPropertyListMaker(new PropertyListMaker() { diff --git a/src/java/org/apache/fop/fo/pagination/Root.java b/src/java/org/apache/fop/fo/pagination/Root.java index c670d6e42..6df6e9537 100644 --- a/src/java/org/apache/fop/fo/pagination/Root.java +++ b/src/java/org/apache/fop/fo/pagination/Root.java @@ -21,6 +21,7 @@ package org.apache.fop.fo.pagination; // java import java.util.List; +import java.util.Locale; import org.xml.sax.Locator; @@ -52,6 +53,7 @@ public class Root extends FObj implements CommonAccessibilityHolder { private BookmarkTree bookmarkTree = null; private List destinationList; private List pageSequences; + private Locale locale; // temporary until above list populated private boolean pageSequenceFound = false; @@ -88,6 +90,24 @@ public class Root extends FObj implements CommonAccessibilityHolder { super.bind(pList); commonAccessibility = CommonAccessibility.getInstance(pList); mediaUsage = pList.get(PR_MEDIA_USAGE).getEnum(); + String language = pList.get(PR_LANGUAGE).getString(); + String country = pList.get(PR_COUNTRY).getString(); + if (isLocalePropertySet(language)) { + if (isLocalePropertySet(country)) { + locale = new Locale(language, country); + } else { + locale = new Locale(language); + } + } + } + + private boolean isLocalePropertySet(String property) { + return property != null && !property.equals("none"); + } + + /** {@inheritDoc} */ + protected void startOfNode() throws FOPException { + foEventHandler.startRoot(this); } /** {@inheritDoc} */ @@ -96,6 +116,7 @@ public class Root extends FObj implements CommonAccessibilityHolder { missingChildElementError("(layout-master-set, declarations?, " + "bookmark-tree?, (page-sequence|fox:external-document)+)"); } + foEventHandler.endRoot(this); } /** @@ -343,4 +364,9 @@ public class Root extends FObj implements CommonAccessibilityHolder { return FO_ROOT; } + + public Locale getLocale() { + return locale; + } + } diff --git a/src/java/org/apache/fop/pdf/PDFDocument.java b/src/java/org/apache/fop/pdf/PDFDocument.java index cbca3ea8f..e9f42d4b2 100644 --- a/src/java/org/apache/fop/pdf/PDFDocument.java +++ b/src/java/org/apache/fop/pdf/PDFDocument.java @@ -353,25 +353,6 @@ public class PDFDocument { return this.root; } - /** - * Makes sure a Lang entry has been set on the document catalog, setting it - * to a default value if necessary. When accessibility is enabled the - * language must be specified for any text element in the document. - */ - public void enforceLanguageOnRoot() { - if (root.getLanguage() == null) { - String fallbackLanguage; - if (getProfile().getPDFAMode().isPDFA1LevelA()) { - //According to Annex B of ISO-19005-1:2005(E), section B.2 - fallbackLanguage = "x-unknown"; - } else { - //No language has been set on the first page-sequence, so fall back to "en". - fallbackLanguage = "en"; - } - root.setLanguage(fallbackLanguage); - } - } - /** * Get the {@link PDFInfo} object for this document. * diff --git a/src/java/org/apache/fop/pdf/PDFProfile.java b/src/java/org/apache/fop/pdf/PDFProfile.java index fb4575105..b4b43c422 100644 --- a/src/java/org/apache/fop/pdf/PDFProfile.java +++ b/src/java/org/apache/fop/pdf/PDFProfile.java @@ -133,8 +133,12 @@ public class PDFProfile { //---------=== Info and validation methods ===--------- + private String format(String pattern, Object[] args) { + return MessageFormat.format(pattern, args); + } + private String format(String pattern, Object arg) { - return MessageFormat.format(pattern, new Object[] {arg}); + return format(pattern, new Object[] {arg}); } /** Checks if encryption is allowed. */ diff --git a/src/java/org/apache/fop/pdf/PDFRoot.java b/src/java/org/apache/fop/pdf/PDFRoot.java index f71841005..177ca4b2d 100644 --- a/src/java/org/apache/fop/pdf/PDFRoot.java +++ b/src/java/org/apache/fop/pdf/PDFRoot.java @@ -21,6 +21,9 @@ package org.apache.fop.pdf; import java.io.IOException; import java.io.OutputStream; +import java.util.Locale; + +import org.apache.fop.util.LanguageTags; /** * Class representing a Root (/Catalog) object. @@ -69,6 +72,7 @@ public class PDFRoot extends PDFDictionary { setObjectNumber(objnum); put("Type", new PDFName("Catalog")); setRootPages(pages); + setLanguage("x-unknown"); } /** {@inheritDoc} */ @@ -254,10 +258,14 @@ public class PDFRoot extends PDFDictionary { * Sets the language identifier of the document. * @param lang the language identifier of the document. */ - public void setLanguage(String lang) { - if (lang == null) { - throw new NullPointerException("lang must not be null"); + public void setLanguage(Locale locale) { + if (locale == null) { + throw new NullPointerException("locale must not be null"); } + setLanguage(LanguageTags.toLanguageTag(locale)); + } + + private void setLanguage(String lang) { put("Lang", lang); } diff --git a/src/java/org/apache/fop/pdf/PDFStructElem.java b/src/java/org/apache/fop/pdf/PDFStructElem.java index 4fb8cbcd5..6dfb0dc95 100644 --- a/src/java/org/apache/fop/pdf/PDFStructElem.java +++ b/src/java/org/apache/fop/pdf/PDFStructElem.java @@ -21,7 +21,7 @@ package org.apache.fop.pdf; import java.util.Locale; -import org.apache.fop.util.XMLUtil; +import org.apache.fop.util.LanguageTags; /** * Class representing a PDF Structure Element. @@ -145,7 +145,7 @@ public class PDFStructElem extends PDFDictionary { * @param language a value for the Lang entry */ public void setLanguage(Locale language) { - setLanguage(XMLUtil.toRFC3066(language)); + setLanguage(LanguageTags.toLanguageTag(language)); } /** diff --git a/src/java/org/apache/fop/render/AbstractRenderer.java b/src/java/org/apache/fop/render/AbstractRenderer.java index 7ff236d16..ec14e597c 100644 --- a/src/java/org/apache/fop/render/AbstractRenderer.java +++ b/src/java/org/apache/fop/render/AbstractRenderer.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.io.OutputStream; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Set; import org.w3c.dom.Document; @@ -61,11 +62,11 @@ import org.apache.fop.area.inline.Image; import org.apache.fop.area.inline.InlineArea; import org.apache.fop.area.inline.InlineBlockParent; import org.apache.fop.area.inline.InlineParent; +import org.apache.fop.area.inline.InlineViewport; import org.apache.fop.area.inline.Leader; import org.apache.fop.area.inline.Space; import org.apache.fop.area.inline.SpaceArea; import org.apache.fop.area.inline.TextArea; -import org.apache.fop.area.inline.InlineViewport; import org.apache.fop.area.inline.WordArea; import org.apache.fop.fo.Constants; import org.apache.fop.fonts.FontInfo; @@ -152,6 +153,10 @@ public abstract class AbstractRenderer return false; } + /** {@inheritDoc} */ + public void setDocumentLocale(Locale locale) { + } + /** * {@inheritDoc} */ diff --git a/src/java/org/apache/fop/render/Renderer.java b/src/java/org/apache/fop/render/Renderer.java index 76f245251..599b8e260 100644 --- a/src/java/org/apache/fop/render/Renderer.java +++ b/src/java/org/apache/fop/render/Renderer.java @@ -22,6 +22,7 @@ package org.apache.fop.render; // Java import java.io.IOException; import java.io.OutputStream; +import java.util.Locale; import org.apache.fop.apps.FOPException; import org.apache.fop.apps.FOUserAgent; @@ -115,6 +116,12 @@ public interface Renderer { */ boolean supportsOutOfOrder(); + /** + * + * @param locale Locale of the language + */ + void setDocumentLocale(Locale locale); + /** * Tells the renderer to process an item not explicitly placed on the * document (e.g., PDF bookmarks). Note - not all renderers will process diff --git a/src/java/org/apache/fop/render/intermediate/AbstractIFDocumentHandler.java b/src/java/org/apache/fop/render/intermediate/AbstractIFDocumentHandler.java index b156b6c3a..ad4e6ad0e 100644 --- a/src/java/org/apache/fop/render/intermediate/AbstractIFDocumentHandler.java +++ b/src/java/org/apache/fop/render/intermediate/AbstractIFDocumentHandler.java @@ -19,6 +19,13 @@ package org.apache.fop.render.intermediate; +import java.util.Locale; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.accessibility.DummyStructureTreeEventHandler; +import org.apache.fop.accessibility.StructureTreeEventHandler; import org.apache.fop.apps.FOUserAgent; /** @@ -52,6 +59,11 @@ public abstract class AbstractIFDocumentHandler implements IFDocumentHandler { return getContext().getUserAgent(); } + /** {@inheritDoc} */ + public StructureTreeEventHandler getStructureTreeEventHandler() { + return DummyStructureTreeEventHandler.INSTANCE; + } + /** {@inheritDoc} */ public IFDocumentNavigationHandler getDocumentNavigationHandler() { return null; //By default, this is not supported @@ -65,6 +77,10 @@ public abstract class AbstractIFDocumentHandler implements IFDocumentHandler { } } + /** {@inheritDoc} */ + public void setDocumentLocale(Locale locale) { + } + /** {@inheritDoc} */ public void startDocumentHeader() throws IFException { //nop @@ -104,5 +120,4 @@ public abstract class AbstractIFDocumentHandler implements IFDocumentHandler { public void endPageTrailer() throws IFException { //nop } - } diff --git a/src/java/org/apache/fop/render/intermediate/IFConstants.java b/src/java/org/apache/fop/render/intermediate/IFConstants.java index 2b16b343d..dba39bec0 100644 --- a/src/java/org/apache/fop/render/intermediate/IFConstants.java +++ b/src/java/org/apache/fop/render/intermediate/IFConstants.java @@ -39,6 +39,7 @@ public interface IFConstants extends XMLConstants { String EL_HEADER = "header"; /** element name trailer */ String EL_TRAILER = "trailer"; + String EL_LOCALE = "locale"; /** element name page-sequence */ String EL_PAGE_SEQUENCE = "page-sequence"; /** element name page */ diff --git a/src/java/org/apache/fop/render/intermediate/IFDocumentHandler.java b/src/java/org/apache/fop/render/intermediate/IFDocumentHandler.java index af1451fe3..6cb8f2795 100644 --- a/src/java/org/apache/fop/render/intermediate/IFDocumentHandler.java +++ b/src/java/org/apache/fop/render/intermediate/IFDocumentHandler.java @@ -20,9 +20,11 @@ package org.apache.fop.render.intermediate; import java.awt.Dimension; +import java.util.Locale; import javax.xml.transform.Result; +import org.apache.fop.accessibility.StructureTreeEventHandler; import org.apache.fop.fonts.FontInfo; /** @@ -32,6 +34,7 @@ import org.apache.fop.fonts.FontInfo; *

*

  * startDocument()
+ *   [setDocumentLocale()]
  *   startDocumentHeader()
  *   [handleExtension()]*
  *   endDocumentHeader()
@@ -117,6 +120,11 @@ public interface IFDocumentHandler {
      */
     IFDocumentHandlerConfigurator getConfigurator();
 
+    /**
+     * @return the structure tree builder
+     */
+    StructureTreeEventHandler getStructureTreeEventHandler();
+
     /**
      * Returns a document navigation handler if this feature is supported.
      * @return the document navigation handler or null if not supported
@@ -151,6 +159,11 @@ public interface IFDocumentHandler {
      */
     void endDocument() throws IFException;
 
+    /**
+    * @param locale Locale of the document.
+    */
+    void setDocumentLocale(Locale locale);
+
     /**
      * Indicates the start of the document header. This method is called right after the
      * {@link #startDocument()} method. Extensions sent to this painter between
@@ -261,7 +274,4 @@ public interface IFDocumentHandler {
      * @throws IFException if an error occurs while handling this event
      */
     void handleExtensionObject(Object extension) throws IFException;
-
-    //TODO Prototype the following:
-    //ContentHandler handleExtension() throws Exception
 }
diff --git a/src/java/org/apache/fop/render/intermediate/IFParser.java b/src/java/org/apache/fop/render/intermediate/IFParser.java
index 313cee685..8f0bb88ec 100644
--- a/src/java/org/apache/fop/render/intermediate/IFParser.java
+++ b/src/java/org/apache/fop/render/intermediate/IFParser.java
@@ -25,6 +25,7 @@ import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.geom.AffineTransform;
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
@@ -48,7 +49,7 @@ import org.apache.commons.logging.LogFactory;
 import org.apache.xmlgraphics.util.QName;
 
 import org.apache.fop.accessibility.AccessibilityEventProducer;
-import org.apache.fop.accessibility.StructureTreeBuilder;
+import org.apache.fop.accessibility.StructureTreeEventHandler;
 import org.apache.fop.apps.FOUserAgent;
 import org.apache.fop.fo.ElementMapping;
 import org.apache.fop.fo.ElementMappingRegistry;
@@ -62,7 +63,7 @@ import org.apache.fop.util.ContentHandlerFactory;
 import org.apache.fop.util.ContentHandlerFactoryRegistry;
 import org.apache.fop.util.DOMBuilderContentHandlerFactory;
 import org.apache.fop.util.DefaultErrorListener;
-import org.apache.fop.util.DelegatingContentHandler;
+import org.apache.fop.util.LanguageTags;
 import org.apache.fop.util.XMLUtil;
 
 /**
@@ -153,24 +154,40 @@ public class IFParser implements IFConstants {
 
         private ContentHandler navParser;
 
-        private StructureTreeBuilder structureTreeBuilder;
-
-        private ContentHandler structureTreeBuilderWrapper;
+        private ContentHandler structureTreeHandler;
 
         private Attributes pageSequenceAttributes;
 
-        private final class StructureTreeBuilderWrapper extends DelegatingContentHandler {
+        private final class StructureTreeHandler extends DefaultHandler {
 
-            private StructureTreeBuilderWrapper()
-                    throws SAXException {
-                super(structureTreeBuilder.getHandlerForNextPageSequence());
+            private final StructureTreeEventHandler structureTreeEventHandler;
+
+            private StructureTreeHandler(StructureTreeEventHandler structureTreeEventHandler,
+                    Locale pageSequenceLanguage) throws SAXException {
+                this.structureTreeEventHandler = structureTreeEventHandler;
+                structureTreeEventHandler.startPageSequence(pageSequenceLanguage);
             }
 
             public void endDocument() throws SAXException {
-                super.endDocument();
                 startIFElement(EL_PAGE_SEQUENCE, pageSequenceAttributes);
                 pageSequenceAttributes = null;
             }
+
+            @Override
+            public void startElement(String uri, String localName, String qName,
+                    Attributes attributes) throws SAXException {
+                if (!"structure-tree".equals(localName)) {
+                    structureTreeEventHandler.startNode(localName, attributes);
+                }
+            }
+
+            @Override
+            public void endElement(String uri, String localName, String arqNameg2)
+                    throws SAXException {
+                if (!"structure-tree".equals(localName)) {
+                    structureTreeEventHandler.endNode(localName);
+                }
+            }
         }
 
         public Handler(IFDocumentHandler documentHandler, FOUserAgent userAgent,
@@ -180,6 +197,7 @@ public class IFParser implements IFConstants {
             this.elementMappingRegistry = elementMappingRegistry;
             elementHandlers.put(EL_DOCUMENT, new DocumentHandler());
             elementHandlers.put(EL_HEADER, new DocumentHeaderHandler());
+            elementHandlers.put(EL_LOCALE, new LocaleHandler());
             elementHandlers.put(EL_TRAILER, new DocumentTrailerHandler());
             elementHandlers.put(EL_PAGE_SEQUENCE, new PageSequenceHandler());
             elementHandlers.put(EL_PAGE, new PageHandler());
@@ -197,11 +215,6 @@ public class IFParser implements IFConstants {
             elementHandlers.put(EL_LINE, new LineHandler());
             elementHandlers.put(EL_BORDER_RECT, new BorderRectHandler());
             elementHandlers.put(EL_IMAGE, new ImageHandler());
-
-            if (userAgent.isAccessibilityEnabled()) {
-                structureTreeBuilder = new StructureTreeBuilder(tFactory);
-                userAgent.setStructureTree(structureTreeBuilder.getStructureTree());
-            }
         }
 
         private void establishForeignAttributes(Map foreignAttributes) {
@@ -231,10 +244,13 @@ public class IFParser implements IFConstants {
                 if (NAMESPACE.equals(uri)) {
                     if (localName.equals(EL_PAGE_SEQUENCE) && userAgent.isAccessibilityEnabled()) {
                         pageSequenceAttributes = new AttributesImpl(attributes);
-                        structureTreeBuilderWrapper = new StructureTreeBuilderWrapper();
+                        Locale language = getLanguage(attributes);
+                        structureTreeHandler = new StructureTreeHandler(
+                                userAgent.getStructureTreeEventHandler(), language);
+
                     } else if (localName.equals(EL_STRUCTURE_TREE)) {
                         if (userAgent.isAccessibilityEnabled()) {
-                            delegate = structureTreeBuilderWrapper;
+                            delegate = structureTreeHandler;
                         } else {
                             /* Delegate to a handler that does nothing */
                             delegate = new DefaultHandler();
@@ -299,6 +315,11 @@ public class IFParser implements IFConstants {
             }
         }
 
+        private static Locale getLanguage(Attributes attributes) {
+            String xmllang = attributes.getValue(XML_NAMESPACE, "lang");
+            return (xmllang == null) ? null : LanguageTags.toLocale(xmllang);
+        }
+
         private boolean startIFElement(String localName, Attributes attributes)
                 throws SAXException {
             lastAttributes = new AttributesImpl(attributes);
@@ -413,6 +434,12 @@ public class IFParser implements IFConstants {
 
         }
 
+        private class LocaleHandler extends AbstractElementHandler {
+            public void startElement(Attributes attributes) throws IFException {
+                documentHandler.setDocumentLocale(getLanguage(attributes));
+            }
+        }
+
         private class DocumentTrailerHandler extends AbstractElementHandler {
 
             public void startElement(Attributes attributes) throws IFException {
@@ -429,10 +456,9 @@ public class IFParser implements IFConstants {
 
             public void startElement(Attributes attributes) throws IFException {
                 String id = attributes.getValue("id");
-                String xmllang = attributes.getValue(XML_NAMESPACE, "lang");
-                if (xmllang != null) {
-                    documentHandler.getContext().setLanguage(
-                            XMLUtil.convertRFC3066ToLocale(xmllang));
+                Locale language = getLanguage(attributes);
+                if (language != null) {
+                    documentHandler.getContext().setLanguage(language);
                 }
                 Map foreignAttributes = getForeignAttributes(lastAttributes);
                 establishForeignAttributes(foreignAttributes);
diff --git a/src/java/org/apache/fop/render/intermediate/IFRenderer.java b/src/java/org/apache/fop/render/intermediate/IFRenderer.java
index 24a2e8a75..d217646f6 100644
--- a/src/java/org/apache/fop/render/intermediate/IFRenderer.java
+++ b/src/java/org/apache/fop/render/intermediate/IFRenderer.java
@@ -51,6 +51,7 @@ import org.apache.xmlgraphics.xmp.schemas.XMPBasicSchema;
 
 import org.apache.fop.Version;
 import org.apache.fop.apps.FOPException;
+import org.apache.fop.apps.FOUserAgent;
 import org.apache.fop.apps.MimeConstants;
 import org.apache.fop.area.Area;
 import org.apache.fop.area.AreaTreeObject;
@@ -70,10 +71,10 @@ import org.apache.fop.area.inline.ForeignObject;
 import org.apache.fop.area.inline.Image;
 import org.apache.fop.area.inline.InlineArea;
 import org.apache.fop.area.inline.InlineParent;
+import org.apache.fop.area.inline.InlineViewport;
 import org.apache.fop.area.inline.Leader;
 import org.apache.fop.area.inline.SpaceArea;
 import org.apache.fop.area.inline.TextArea;
-import org.apache.fop.area.inline.InlineViewport;
 import org.apache.fop.area.inline.WordArea;
 import org.apache.fop.datatypes.URISpecification;
 import org.apache.fop.fo.extensions.ExtensionAttachment;
@@ -227,7 +228,11 @@ public class IFRenderer extends AbstractPathOrientedRenderer {
      */
     protected IFDocumentHandler createDefaultDocumentHandler() {
         IFSerializer serializer = new IFSerializer();
-        serializer.setContext(new IFContext(getUserAgent()));
+        FOUserAgent userAgent = getUserAgent();
+        serializer.setContext(new IFContext(userAgent));
+        if (userAgent.isAccessibilityEnabled()) {
+            userAgent.setStructureTreeEventHandler(serializer.getStructureTreeEventHandler());
+        }
         return serializer;
     }
 
@@ -294,6 +299,11 @@ public class IFRenderer extends AbstractPathOrientedRenderer {
         log.debug("Rendering finished.");
     }
 
+    @Override
+    public void setDocumentLocale(Locale locale) {
+        documentHandler.setDocumentLocale(locale);
+    }
+
     /** {@inheritDoc} */
     public void processOffDocumentItem(OffDocumentItem odi) {
         if (odi instanceof DestinationData) {
diff --git a/src/java/org/apache/fop/render/intermediate/IFSerializer.java b/src/java/org/apache/fop/render/intermediate/IFSerializer.java
index 471a982ca..a4431b972 100644
--- a/src/java/org/apache/fop/render/intermediate/IFSerializer.java
+++ b/src/java/org/apache/fop/render/intermediate/IFSerializer.java
@@ -31,16 +31,13 @@ import java.util.Locale;
 import java.util.Map;
 
 import org.w3c.dom.Document;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-
 import org.xml.sax.SAXException;
 import org.xml.sax.helpers.AttributesImpl;
 
 import org.apache.xmlgraphics.util.QName;
 import org.apache.xmlgraphics.util.XMLizable;
 
-import org.apache.fop.accessibility.StructureTree;
+import org.apache.fop.accessibility.StructureTreeEventHandler;
 import org.apache.fop.fonts.FontInfo;
 import org.apache.fop.render.PrintRendererConfigurator;
 import org.apache.fop.render.RenderingContext;
@@ -54,9 +51,11 @@ import org.apache.fop.traits.BorderProps;
 import org.apache.fop.traits.RuleStyle;
 import org.apache.fop.util.ColorUtil;
 import org.apache.fop.util.DOM2SAX;
+import org.apache.fop.util.LanguageTags;
 import org.apache.fop.util.XMLConstants;
 import org.apache.fop.util.XMLUtil;
 
+
 /**
  * IFPainter implementation that serializes the intermediate format to XML.
  */
@@ -71,11 +70,7 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler
 
     private String currentID = "";
 
-    /**
-     * Default constructor.
-     */
-    public IFSerializer() {
-    }
+    private IFStructureTreeBuilder structureTreeBuilder;
 
     /** {@inheritDoc} */
     @Override
@@ -150,6 +145,14 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler
         }
     }
 
+    @Override
+    public StructureTreeEventHandler getStructureTreeEventHandler() {
+        if (structureTreeBuilder == null) {
+            structureTreeBuilder = new IFStructureTreeBuilder();
+        }
+        return structureTreeBuilder;
+    }
+
     /** {@inheritDoc} */
     @Override
     public void startDocument() throws IFException {
@@ -166,6 +169,19 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler
         }
     }
 
+    @Override
+    public void setDocumentLocale(Locale locale) {
+        AttributesImpl atts  = new AttributesImpl();
+        atts.addAttribute(XML_NAMESPACE, "lang", "xml:lang", XMLUtil.CDATA,
+                LanguageTags.toLanguageTag(locale));
+        try {
+            handler.startElement(EL_LOCALE, atts);
+            handler.endElement(EL_LOCALE);
+        } catch (SAXException e) {
+            throw new RuntimeException("Unable to create the " + EL_LOCALE + " element.", e);
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     public void startDocumentHeader() throws IFException {
@@ -227,20 +243,14 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler
             Locale lang = getContext().getLanguage();
             if (lang != null) {
                 atts.addAttribute(XML_NAMESPACE, "lang", "xml:lang", XMLUtil.CDATA,
-                        XMLUtil.toRFC3066(lang));
+                        LanguageTags.toLanguageTag(lang));
             }
             XMLUtil.addAttribute(atts, XMLConstants.XML_SPACE, "preserve");
             addForeignAttributes(atts);
             handler.startElement(EL_PAGE_SEQUENCE, atts);
             if (this.getUserAgent().isAccessibilityEnabled()) {
-                StructureTree structureTree = getUserAgent().getStructureTree();
-                handler.startElement(EL_STRUCTURE_TREE); // add structure tree
-                NodeList nodes = structureTree.getPageSequence(pageSequenceIndex++);
-                for (int i = 0, n = nodes.getLength(); i < n; i++) {
-                    Node node = nodes.item(i);
-                    new DOM2SAX(handler).writeFragment(node);
-                }
-                handler.endElement(EL_STRUCTURE_TREE);
+                assert (structureTreeBuilder != null);
+                structureTreeBuilder.replayEventsForPageSequence(handler, pageSequenceIndex++);
             }
         } catch (SAXException e) {
             throw new IFException("SAX error in startPageSequence()", e);
@@ -250,6 +260,7 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler
     /** {@inheritDoc} */
     public void endPageSequence() throws IFException {
         try {
+
             handler.endElement(EL_PAGE_SEQUENCE);
         } catch (SAXException e) {
             throw new IFException("SAX error in endPageSequence()", e);
@@ -806,5 +817,4 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler
             throw new IFException("SAX error serializing object", e);
         }
     }
-
 }
diff --git a/src/java/org/apache/fop/render/intermediate/IFSerializerMaker.java b/src/java/org/apache/fop/render/intermediate/IFSerializerMaker.java
index 699fd0549..c0060ab8e 100644
--- a/src/java/org/apache/fop/render/intermediate/IFSerializerMaker.java
+++ b/src/java/org/apache/fop/render/intermediate/IFSerializerMaker.java
@@ -31,6 +31,9 @@ public class IFSerializerMaker extends AbstractIFDocumentHandlerMaker {
     public IFDocumentHandler makeIFDocumentHandler(FOUserAgent ua) {
         IFSerializer handler = new IFSerializer();
         handler.setContext(new IFContext(ua));
+        if (ua.isAccessibilityEnabled()) {
+            ua.setStructureTreeEventHandler(handler.getStructureTreeEventHandler());
+        }
         return handler;
     }
 
diff --git a/src/java/org/apache/fop/render/intermediate/IFStructureTreeBuilder.java b/src/java/org/apache/fop/render/intermediate/IFStructureTreeBuilder.java
new file mode 100644
index 000000000..66457681f
--- /dev/null
+++ b/src/java/org/apache/fop/render/intermediate/IFStructureTreeBuilder.java
@@ -0,0 +1,190 @@
+/*
+ * 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.render.intermediate;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.fop.accessibility.StructureTree2SAXEventAdapter;
+import org.apache.fop.accessibility.StructureTreeEventHandler;
+
+/**
+ * Saves structure tree events as SAX events in order to replay them when it's
+ * time to stream the structure tree to the output.
+ */
+final class IFStructureTreeBuilder implements StructureTreeEventHandler {
+
+    private StructureTreeEventHandler delegate;
+
+    private final List pageSequenceEventRecorders = new ArrayList();
+
+    /**
+     * Replay SAX events for a page sequence.
+     * @param handler The handler that receives SAX events
+     * @param pageSequenceIndex The index of the page sequence
+     * @throws SAXException
+     */
+    public void replayEventsForPageSequence(ContentHandler handler,
+            int pageSequenceIndex) throws SAXException {
+        pageSequenceEventRecorders.get(pageSequenceIndex).replay(handler);
+    }
+
+    /** {@inheritDoc} */
+    public void startPageSequence(Locale locale) {
+        SAXEventRecorder eventRecorder = new SAXEventRecorder();
+        pageSequenceEventRecorders.add(eventRecorder);
+        delegate = StructureTree2SAXEventAdapter.newInstance(eventRecorder);
+        delegate.startPageSequence(locale);
+    }
+
+    /** {@inheritDoc} */
+    public void endPageSequence() {
+         delegate.endPageSequence();
+    }
+
+    /** {@inheritDoc} */
+    public void startNode(String name, Attributes attributes) {
+        delegate.startNode(name, attributes);
+    }
+
+    /** {@inheritDoc} */
+    public void endNode(String name) {
+        delegate.endNode(name);
+    }
+
+    /** A SAX handler that records events to replay them later. */
+    static class SAXEventRecorder extends DefaultHandler {
+
+        private final List events = new ArrayList();
+
+        private abstract static class Event {
+            abstract void replay(ContentHandler handler) throws SAXException;
+        }
+
+        private abstract static class Element extends SAXEventRecorder.Event {
+
+            protected final String uri;
+            protected final String localName;
+            protected final String qName;
+
+            private Element(String uri, String localName, String qName) {
+                this.uri = uri;
+                this.localName = localName;
+                this.qName = qName;
+            }
+        }
+
+        private static final class StartElement extends SAXEventRecorder.Element {
+
+            private final Attributes attributes;
+
+            private StartElement(String uri, String localName, String qName,
+                    Attributes attributes) {
+                super(uri, localName, qName);
+                this.attributes = attributes;
+            }
+
+            @Override
+            void replay(ContentHandler handler) throws SAXException {
+                handler.startElement(uri, localName, qName, attributes);
+            }
+        }
+
+        private static final class EndElement extends SAXEventRecorder.Element {
+
+            private EndElement(String uri, String localName, String qName) {
+                super(uri, localName, qName);
+            }
+
+            @Override
+            void replay(ContentHandler handler) throws SAXException {
+                handler.endElement(uri, localName, qName);
+            }
+        }
+
+        private static final class StartPrefixMapping extends SAXEventRecorder.Event {
+
+            protected final String prefix;
+            protected final String uri;
+
+            private StartPrefixMapping(String prefix, String uri) {
+                this.prefix = prefix;
+                this.uri = uri;
+            }
+
+            @Override
+            void replay(ContentHandler handler) throws SAXException {
+                handler.startPrefixMapping(prefix, uri);
+            }
+        }
+
+        private static final class EndPrefixMapping extends SAXEventRecorder.Event {
+
+            protected final String prefix;
+
+            private EndPrefixMapping(String prefix) {
+                this.prefix = prefix;
+            }
+
+            @Override
+            void replay(ContentHandler handler) throws SAXException {
+                handler.endPrefixMapping(prefix);
+            }
+        }
+
+        @Override
+        public void startElement(String uri, String localName, String qName,
+                Attributes attributes) throws SAXException {
+            events.add(new StartElement(uri, localName, qName, attributes));
+        };
+
+        @Override
+        public void endElement(String uri, String localName, String qName) throws SAXException {
+            events.add(new EndElement(uri, localName, qName));
+        };
+
+        @Override
+        public void startPrefixMapping(String prefix, String uri) throws SAXException {
+            events.add(new StartPrefixMapping(prefix, uri));
+        };
+
+        @Override
+        public void endPrefixMapping(String prefix) throws SAXException {
+            events.add(new EndPrefixMapping(prefix));
+        };
+
+        /**
+         * Replays the recorded events.
+         *
+         * @param handler {@code ContentHandler} to replay events on
+         */
+        public void replay(ContentHandler handler) throws SAXException {
+            for (SAXEventRecorder.Event e : events) {
+                e.replay(handler);
+            }
+        }
+    }
+}
diff --git a/src/java/org/apache/fop/render/intermediate/util/IFDocumentHandlerProxy.java b/src/java/org/apache/fop/render/intermediate/util/IFDocumentHandlerProxy.java
index 3d89e812e..64c876fb1 100644
--- a/src/java/org/apache/fop/render/intermediate/util/IFDocumentHandlerProxy.java
+++ b/src/java/org/apache/fop/render/intermediate/util/IFDocumentHandlerProxy.java
@@ -20,9 +20,12 @@
 package org.apache.fop.render.intermediate.util;
 
 import java.awt.Dimension;
+import java.util.Locale;
 
 import javax.xml.transform.Result;
 
+import org.apache.fop.accessibility.DummyStructureTreeEventHandler;
+import org.apache.fop.accessibility.StructureTreeEventHandler;
 import org.apache.fop.fonts.FontInfo;
 import org.apache.fop.render.intermediate.IFContext;
 import org.apache.fop.render.intermediate.IFDocumentHandler;
@@ -93,6 +96,11 @@ public class IFDocumentHandlerProxy implements IFDocumentHandler {
         return this.delegate.getDocumentNavigationHandler();
     }
 
+    /** {@inheritDoc} */
+    public StructureTreeEventHandler getStructureTreeEventHandler() {
+        return DummyStructureTreeEventHandler.INSTANCE;
+    }
+
     /** {@inheritDoc} */
     public void setResult(Result result) throws IFException {
         this.delegate.setResult(result);
@@ -103,6 +111,12 @@ public class IFDocumentHandlerProxy implements IFDocumentHandler {
         this.delegate.startDocument();
     }
 
+    /** {@inheritDoc} */
+    public void setDocumentLocale(Locale locale) {
+         this.delegate.setDocumentLocale(locale);
+
+    }
+
     /** {@inheritDoc} */
     public void startDocumentHeader() throws IFException {
         this.delegate.startDocumentHeader();
@@ -184,4 +198,4 @@ public class IFDocumentHandlerProxy implements IFDocumentHandler {
         this.delegate.handleExtensionObject(extension);
     }
 
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/fop/render/pdf/FOToPDFRoleMap.java b/src/java/org/apache/fop/render/pdf/FOToPDFRoleMap.java
index 26595bca1..c773b1339 100644
--- a/src/java/org/apache/fop/render/pdf/FOToPDFRoleMap.java
+++ b/src/java/org/apache/fop/render/pdf/FOToPDFRoleMap.java
@@ -22,8 +22,6 @@ package org.apache.fop.render.pdf;
 import java.util.HashMap;
 import java.util.Map;
 
-import org.w3c.dom.Node;
-
 import org.apache.fop.events.EventBroadcaster;
 import org.apache.fop.pdf.PDFName;
 import org.apache.fop.pdf.PDFObject;
@@ -37,9 +35,9 @@ final class FOToPDFRoleMap {
     /**
      * Standard structure types defined by the PDF Reference, Fourth Edition (PDF 1.5).
      */
-    private static final Map STANDARD_STRUCTURE_TYPES = new HashMap();
+    private static final Map STANDARD_STRUCTURE_TYPES = new HashMap();
 
-    private static final Map DEFAULT_MAPPINGS = new java.util.HashMap();
+    private static final Map DEFAULT_MAPPINGS = new java.util.HashMap();
 
     private static final PDFName THEAD;
     private static final PDFName NON_STRUCT;
@@ -172,7 +170,7 @@ final class FOToPDFRoleMap {
      * @return the structure type or null if no match could be found
      */
     public static PDFName mapFormattingObject(String fo, PDFObject parent) {
-        Mapper mapper = (Mapper)DEFAULT_MAPPINGS.get(fo);
+        Mapper mapper = (Mapper) DEFAULT_MAPPINGS.get(fo);
         if (mapper != null) {
             return mapper.getStructureType(parent);
         } else {
@@ -180,27 +178,32 @@ final class FOToPDFRoleMap {
         }
     }
 
-    public static PDFName mapFormattingObject(Node fo, PDFObject parent,
-            EventBroadcaster eventBroadcaster) {
+    /**
+     * Maps a Formatting Object to a PDFName representing the associated structure type.
+     * @param fo the formatting object's local name
+     * @param role the value of the formatting object's role property
+     * @param parent the parent of the structure element to be mapped
+     * @param eventBroadcaster the event broadcaster
+     * @return the structure type or null if no match could be found
+     */
+    public static PDFName mapFormattingObject(String fo, String role,
+            PDFObject parent, EventBroadcaster eventBroadcaster) {
         PDFName type = null;
-        Node role = fo.getAttributes().getNamedItemNS(null, "role");
         if (role == null) {
-            type = mapFormattingObject(fo.getLocalName(), parent);
+            type = mapFormattingObject(fo, parent);
         } else {
-            String customType = role.getNodeValue();
-            type = (PDFName) STANDARD_STRUCTURE_TYPES.get(customType);
+            type = (PDFName) STANDARD_STRUCTURE_TYPES.get(role);
             if (type == null) {
-                String foName = fo.getLocalName();
-                type = mapFormattingObject(foName, parent);
+                type = mapFormattingObject(fo, parent);
                 PDFEventProducer.Provider.get(eventBroadcaster).nonStandardStructureType(fo,
-                        foName, customType, type.toString().substring(1));
+                        fo, role, type.toString().substring(1));
             }
         }
         assert type != null;
         return type;
     }
 
-    private static interface Mapper {
+    private interface Mapper {
         PDFName getStructureType(PDFObject parent);
     }
 
@@ -222,7 +225,7 @@ final class FOToPDFRoleMap {
 
         public PDFName getStructureType(PDFObject parent) {
             PDFStructElem grandParent = (PDFStructElem)
-                ((PDFStructElem)parent).getParentStructElem();
+                ((PDFStructElem) parent).getParentStructElem();
             //TODO What to do with cells from table-footer? Currently they are mapped on TD.
             PDFName type;
             if (THEAD.equals(grandParent.getStructureType())) {
diff --git a/src/java/org/apache/fop/render/pdf/PDFDocumentHandler.java b/src/java/org/apache/fop/render/pdf/PDFDocumentHandler.java
index c1d959dd1..5be8c3001 100644
--- a/src/java/org/apache/fop/render/pdf/PDFDocumentHandler.java
+++ b/src/java/org/apache/fop/render/pdf/PDFDocumentHandler.java
@@ -25,15 +25,16 @@ import java.awt.geom.AffineTransform;
 import java.awt.geom.Point2D;
 import java.awt.geom.Rectangle2D;
 import java.io.IOException;
+import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 
-import org.w3c.dom.NodeList;
-
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
 import org.apache.xmlgraphics.xmp.Metadata;
 
+import org.apache.fop.accessibility.StructureTreeEventHandler;
 import org.apache.fop.apps.MimeConstants;
 import org.apache.fop.fo.extensions.xmp.XMPMetadata;
 import org.apache.fop.pdf.PDFAnnotList;
@@ -45,28 +46,26 @@ import org.apache.fop.render.extensions.prepress.PageBoundaries;
 import org.apache.fop.render.extensions.prepress.PageScale;
 import org.apache.fop.render.intermediate.AbstractBinaryWritingIFDocumentHandler;
 import org.apache.fop.render.intermediate.IFContext;
-import org.apache.fop.render.intermediate.IFDocumentHandler;
 import org.apache.fop.render.intermediate.IFDocumentHandlerConfigurator;
 import org.apache.fop.render.intermediate.IFDocumentNavigationHandler;
 import org.apache.fop.render.intermediate.IFException;
 import org.apache.fop.render.intermediate.IFPainter;
 import org.apache.fop.render.pdf.extensions.PDFEmbeddedFileExtensionAttachment;
-import org.apache.fop.util.XMLUtil;
 
 /**
- * {@link IFDocumentHandler} implementation that produces PDF.
+ * {@link org.apache.fop.render.intermediate.IFDocumentHandler} implementation that produces PDF.
  */
 public class PDFDocumentHandler extends AbstractBinaryWritingIFDocumentHandler {
 
     /** logging instance */
     private static Log log = LogFactory.getLog(PDFDocumentHandler.class);
 
-    private int pageSequenceIndex;
-
     private boolean accessEnabled;
 
     private PDFLogicalStructureHandler logicalStructureHandler;
 
+    private PDFStructureTreeBuilder structureTreeBuilder;
+
     /** the PDF Document being created */
     protected PDFDocument pdfDoc;
 
@@ -92,8 +91,7 @@ public class PDFDocumentHandler extends AbstractBinaryWritingIFDocumentHandler {
     protected PageReference currentPageRef;
 
     /** Used for bookmarks/outlines. */
-    protected Map pageReferences
-        = new java.util.HashMap();
+    protected Map pageReferences = new HashMap();
 
     private final PDFDocumentNavigationHandler documentNavigationHandler
             = new PDFDocumentNavigationHandler(this);
@@ -145,15 +143,23 @@ public class PDFDocumentHandler extends AbstractBinaryWritingIFDocumentHandler {
             this.pdfDoc = pdfUtil.setupPDFDocument(this.outputStream);
             this.accessEnabled = getUserAgent().isAccessibilityEnabled();
             if (accessEnabled) {
-                pdfDoc.getRoot().makeTagged();
-                logicalStructureHandler = new PDFLogicalStructureHandler(pdfDoc,
-                        getUserAgent().getEventBroadcaster());
+                setupAccessibility();
             }
         } catch (IOException e) {
             throw new IFException("I/O error in startDocument()", e);
         }
     }
 
+    private void setupAccessibility() {
+        pdfDoc.getRoot().makeTagged();
+        logicalStructureHandler = new PDFLogicalStructureHandler(pdfDoc);
+        // TODO this is ugly. All the necessary information should be available
+        // at creation time in order to enforce immutability
+        structureTreeBuilder.setPdfFactory(pdfDoc.getFactory());
+        structureTreeBuilder.setLogicalStructureHandler(logicalStructureHandler);
+        structureTreeBuilder.setEventBroadcaster(getUserAgent().getEventBroadcaster());
+    }
+
     /** {@inheritDoc} */
     public void endDocumentHeader() throws IFException {
         pdfUtil.generateDefaultXMPMetadata();
@@ -178,18 +184,7 @@ public class PDFDocumentHandler extends AbstractBinaryWritingIFDocumentHandler {
 
     /** {@inheritDoc} */
     public void startPageSequence(String id) throws IFException {
-        //TODO page sequence title
-
-        if (this.pdfDoc.getRoot().getLanguage() == null
-                && getContext().getLanguage() != null) {
-            //No document-level language set, so we use the first page-sequence's language
-            this.pdfDoc.getRoot().setLanguage(XMLUtil.toRFC3066(getContext().getLanguage()));
-        }
-
-        if (accessEnabled) {
-            NodeList nodes = getUserAgent().getStructureTree().getPageSequence(pageSequenceIndex++);
-            logicalStructureHandler.processStructureTree(nodes, getContext().getLanguage());
-        }
+        //nop
     }
 
     /** {@inheritDoc} */
@@ -289,9 +284,9 @@ public class PDFDocumentHandler extends AbstractBinaryWritingIFDocumentHandler {
     /** {@inheritDoc} */
     public void handleExtensionObject(Object extension) throws IFException {
         if (extension instanceof XMPMetadata) {
-            pdfUtil.renderXMPMetadata((XMPMetadata)extension);
+            pdfUtil.renderXMPMetadata((XMPMetadata) extension);
         } else if (extension instanceof Metadata) {
-            XMPMetadata wrapper = new XMPMetadata(((Metadata)extension));
+            XMPMetadata wrapper = new XMPMetadata(((Metadata) extension));
             pdfUtil.renderXMPMetadata(wrapper);
         } else if (extension instanceof PDFEmbeddedFileExtensionAttachment) {
             PDFEmbeddedFileExtensionAttachment embeddedFile
@@ -307,6 +302,11 @@ public class PDFDocumentHandler extends AbstractBinaryWritingIFDocumentHandler {
         }
     }
 
+    /** {@inheritDoc} */
+    public void setDocumentLocale(Locale locale) {
+        pdfDoc.getRoot().setLanguage(locale);
+    }
+
     PageReference getPageReference(int pageIndex) {
         return this.pageReferences.get(Integer.valueOf(pageIndex));
     }
@@ -332,4 +332,11 @@ public class PDFDocumentHandler extends AbstractBinaryWritingIFDocumentHandler {
         }
     }
 
+    @Override
+    public StructureTreeEventHandler getStructureTreeEventHandler() {
+        if (structureTreeBuilder == null) {
+            structureTreeBuilder = new PDFStructureTreeBuilder();
+        }
+        return structureTreeBuilder;
+    }
 }
diff --git a/src/java/org/apache/fop/render/pdf/PDFDocumentHandlerMaker.java b/src/java/org/apache/fop/render/pdf/PDFDocumentHandlerMaker.java
index f4ba03d5b..1186db3ae 100644
--- a/src/java/org/apache/fop/render/pdf/PDFDocumentHandlerMaker.java
+++ b/src/java/org/apache/fop/render/pdf/PDFDocumentHandlerMaker.java
@@ -36,6 +36,9 @@ public class PDFDocumentHandlerMaker extends AbstractIFDocumentHandlerMaker {
     public IFDocumentHandler makeIFDocumentHandler(FOUserAgent ua) {
         PDFDocumentHandler handler = new PDFDocumentHandler();
         handler.setContext(new IFContext(ua));
+        if (ua.isAccessibilityEnabled()) {
+            ua.setStructureTreeEventHandler(handler.getStructureTreeEventHandler());
+        }
         return handler;
     }
 
diff --git a/src/java/org/apache/fop/render/pdf/PDFLogicalStructureHandler.java b/src/java/org/apache/fop/render/pdf/PDFLogicalStructureHandler.java
index ce86f4849..d49ef4a3e 100644
--- a/src/java/org/apache/fop/render/pdf/PDFLogicalStructureHandler.java
+++ b/src/java/org/apache/fop/render/pdf/PDFLogicalStructureHandler.java
@@ -23,12 +23,6 @@ import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
 
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-
-import org.apache.fop.events.EventBroadcaster;
-import org.apache.fop.fo.extensions.ExtensionElementMapping;
-import org.apache.fop.fo.extensions.InternalElementMapping;
 import org.apache.fop.pdf.PDFArray;
 import org.apache.fop.pdf.PDFDictionary;
 import org.apache.fop.pdf.PDFDocument;
@@ -53,12 +47,10 @@ class PDFLogicalStructureHandler {
 
     private final PDFDocument pdfDoc;
 
-    private final EventBroadcaster eventBroadcaster;
-
     /**
      * Map of references to the corresponding structure elements.
      */
-    private final Map structTreeMap = new HashMap();
+    private final Map structTreeMap = new HashMap();
 
     private final PDFParentTree parentTree = new PDFParentTree();
 
@@ -108,23 +100,16 @@ class PDFLogicalStructureHandler {
      *
      * @param pdfDoc a document
      */
-    PDFLogicalStructureHandler(PDFDocument pdfDoc, EventBroadcaster eventBroadcaster) {
+    PDFLogicalStructureHandler(PDFDocument pdfDoc) {
         this.pdfDoc = pdfDoc;
-        this.eventBroadcaster = eventBroadcaster;
         PDFStructTreeRoot structTreeRoot = pdfDoc.getFactory().makeStructTreeRoot(parentTree);
         rootStructureElement = pdfDoc.getFactory().makeStructureElement(
                 FOToPDFRoleMap.mapFormattingObject("root", structTreeRoot), structTreeRoot);
         structTreeRoot.addKid(rootStructureElement);
     }
 
-    /**
-     * Converts the given structure tree into PDF.
-     *
-     * @param structureTree the structure tree of the current page sequence
-     * @param language language set on the page sequence
-     */
-    void processStructureTree(NodeList structureTree, Locale language) {
-        pdfDoc.enforceLanguageOnRoot();
+
+    PDFStructElem createPageSequence(Locale language) {
         PDFStructElem structElemPart = pdfDoc.getFactory().makeStructureElement(
                 FOToPDFRoleMap.mapFormattingObject("page-sequence", rootStructureElement),
                 rootStructureElement);
@@ -132,50 +117,7 @@ class PDFLogicalStructureHandler {
         if (language != null) {
             structElemPart.setLanguage(language);
         }
-
-        for (int i = 0, n = structureTree.getLength(); i < n; i++) {
-            Node node = structureTree.item(i);
-            assert node.getLocalName().equals("flow")
-                    || node.getLocalName().equals("static-content");
-            PDFStructElem structElemSect = pdfDoc.getFactory().makeStructureElement(
-                    FOToPDFRoleMap.mapFormattingObject(node.getLocalName(), structElemPart),
-                    structElemPart);
-            structElemPart.addKid(structElemSect);
-            NodeList childNodes = node.getChildNodes();
-            for (int j = 0, m = childNodes.getLength(); j < m; j++) {
-                processNode(childNodes.item(j), structElemSect, true);
-            }
-        }
-    }
-
-    private void processNode(Node node, PDFStructElem parent, boolean addKid) {
-        PDFStructElem structElem = pdfDoc.getFactory().makeStructureElement(
-                FOToPDFRoleMap.mapFormattingObject(node, parent, eventBroadcaster), parent);
-        // TODO necessary? If a page-sequence is empty (e.g., contains a single
-        // empty fo:block), should the block still be added to the structure
-        // tree? This is not being done for descendant empty elements...
-        if (addKid) {
-            parent.addKid(structElem);
-        }
-        String nodeName = node.getLocalName();
-        if (nodeName.equals("external-graphic") || nodeName.equals("instream-foreign-object")) {
-            Node altTextNode = node.getAttributes().getNamedItemNS(
-                    ExtensionElementMapping.URI, "alt-text");
-            if (altTextNode != null) {
-                structElem.put("Alt", altTextNode.getNodeValue());
-            } else {
-                structElem.put("Alt", "No alternate text specified");
-            }
-        }
-        Node attr = node.getAttributes().getNamedItemNS(InternalElementMapping.URI, "ptr");
-        if (attr != null) {
-            String ptr = attr.getNodeValue();
-            structTreeMap.put(ptr, structElem);
-        }
-        NodeList nodes = node.getChildNodes();
-        for (int i = 0, n = nodes.getLength(); i < n; i++) {
-            processNode(nodes.item(i), structElem, false);
-        }
+        return structElemPart;
     }
 
     private int getNextParentTreeKey() {
@@ -301,4 +243,8 @@ class PDFLogicalStructureHandler {
         parent.addKid(contentItem);
     }
 
+    void addStructurePointer(String ptr, PDFStructElem structElem) {
+        structTreeMap.put(ptr, structElem);
+    }
+
 }
diff --git a/src/java/org/apache/fop/render/pdf/PDFStructureTreeBuilder.java b/src/java/org/apache/fop/render/pdf/PDFStructureTreeBuilder.java
new file mode 100644
index 000000000..8ec10b209
--- /dev/null
+++ b/src/java/org/apache/fop/render/pdf/PDFStructureTreeBuilder.java
@@ -0,0 +1,93 @@
+/*
+ * 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.render.pdf;
+
+import java.util.LinkedList;
+import java.util.Locale;
+
+import org.xml.sax.Attributes;
+
+import org.apache.fop.accessibility.StructureTreeEventHandler;
+import org.apache.fop.events.EventBroadcaster;
+import org.apache.fop.fo.extensions.ExtensionElementMapping;
+import org.apache.fop.fo.extensions.InternalElementMapping;
+import org.apache.fop.pdf.PDFFactory;
+import org.apache.fop.pdf.PDFStructElem;
+
+class PDFStructureTreeBuilder implements StructureTreeEventHandler {
+
+    private PDFFactory pdfFactory;
+
+    private PDFLogicalStructureHandler logicalStructureHandler;
+
+    private EventBroadcaster eventBroadcaster;
+
+    private LinkedList ancestors = new LinkedList();
+
+    void setPdfFactory(PDFFactory pdfFactory) {
+        this.pdfFactory = pdfFactory;
+    }
+
+    void setLogicalStructureHandler(PDFLogicalStructureHandler logicalStructureHandler) {
+        this.logicalStructureHandler = logicalStructureHandler;
+    }
+
+    void setEventBroadcaster(EventBroadcaster eventBroadcaster) {
+        this.eventBroadcaster = eventBroadcaster;
+    }
+
+    public void startPageSequence(Locale locale) {
+        ancestors = new LinkedList();
+        ancestors.add(logicalStructureHandler.createPageSequence(locale));
+    }
+
+    public void endPageSequence() {
+    }
+
+    public void startNode(String name, Attributes attributes) {
+        PDFStructElem parent = ancestors.getFirst();
+        String role = attributes.getValue("role");
+        PDFStructElem created = pdfFactory.makeStructureElement(
+                FOToPDFRoleMap.mapFormattingObject(name, role, parent,
+                        eventBroadcaster), parent);
+        if (ancestors.size() <= 2) { // TODO remove
+            parent.addKid(created);
+        }
+        String ptr = attributes.getValue(InternalElementMapping.URI, "ptr");
+        if (ptr != null) {
+            logicalStructureHandler.addStructurePointer(ptr, created);
+        }
+
+        if (name.equals("external-graphic") || name.equals("instream-foreign-object")) {
+            String altTextNode = attributes.getValue(ExtensionElementMapping.URI, "alt-text");
+            if (altTextNode != null) {
+                created.put("Alt", altTextNode);
+            } else {
+                created.put("Alt", "No alternate text specified");
+            }
+        }
+        ancestors.addFirst(created);
+    }
+
+    public void endNode(String name) {
+        ancestors.removeFirst();
+    }
+
+}
diff --git a/src/java/org/apache/fop/render/xml/XMLRenderer.java b/src/java/org/apache/fop/render/xml/XMLRenderer.java
index 21d28042d..2a62d2cea 100644
--- a/src/java/org/apache/fop/render/xml/XMLRenderer.java
+++ b/src/java/org/apache/fop/render/xml/XMLRenderer.java
@@ -35,9 +35,6 @@ import javax.xml.transform.sax.TransformerHandler;
 import javax.xml.transform.stream.StreamResult;
 
 import org.w3c.dom.Document;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-
 import org.xml.sax.SAXException;
 
 import org.apache.xmlgraphics.util.QName;
@@ -75,23 +72,20 @@ import org.apache.fop.area.inline.Image;
 import org.apache.fop.area.inline.InlineArea;
 import org.apache.fop.area.inline.InlineBlockParent;
 import org.apache.fop.area.inline.InlineParent;
+import org.apache.fop.area.inline.InlineViewport;
 import org.apache.fop.area.inline.Leader;
 import org.apache.fop.area.inline.Space;
 import org.apache.fop.area.inline.SpaceArea;
 import org.apache.fop.area.inline.TextArea;
-import org.apache.fop.area.inline.InlineViewport;
 import org.apache.fop.area.inline.WordArea;
 import org.apache.fop.fo.Constants;
 import org.apache.fop.fo.extensions.ExtensionAttachment;
-import org.apache.fop.fo.extensions.ExtensionElementMapping;
-import org.apache.fop.fo.extensions.InternalElementMapping;
 import org.apache.fop.fonts.FontInfo;
 import org.apache.fop.fonts.FontTriplet;
 import org.apache.fop.render.Renderer;
 import org.apache.fop.render.RendererContext;
 import org.apache.fop.render.XMLHandler;
 import org.apache.fop.util.ColorUtil;
-import org.apache.fop.util.DOM2SAX;
 
 /**
  * Renderer that renders areas to XML for debugging purposes.
@@ -461,29 +455,6 @@ public class XMLRenderer extends AbstractXMLRenderer {
         }
         transferForeignObjects(pageSequence);
         startElement("pageSequence", atts);
-        if (this.getUserAgent().isAccessibilityEnabled()) {
-            String structureTreeElement = "structureTree";
-            startElement(structureTreeElement);
-            try {
-                this.handler.startPrefixMapping("foi", InternalElementMapping.URI);
-                this.handler.startPrefixMapping("fox", ExtensionElementMapping.URI);
-                NodeList nodes = getUserAgent().getStructureTree().getPageSequence(
-                        pageSequenceIndex++);
-                for (int i = 0, n = nodes.getLength(); i < n; i++) {
-                    Node node = nodes.item(i);
-                    try {
-                        new DOM2SAX(handler).writeFragment(node);
-                    } catch (SAXException e) {
-                        handleSAXException(e);
-                    }
-                }
-                this.handler.endPrefixMapping("fox");
-                this.handler.endPrefixMapping("foi");
-            } catch (SAXException se) {
-                handleSAXException(se);
-            }
-            endElement(structureTreeElement);
-        }
         handleExtensionAttachments(pageSequence.getExtensionAttachments());
         LineArea seqTitle = pageSequence.getTitle();
         if (seqTitle != null) {
diff --git a/src/java/org/apache/fop/util/LanguageTags.java b/src/java/org/apache/fop/util/LanguageTags.java
new file mode 100644
index 000000000..070fa15db
--- /dev/null
+++ b/src/java/org/apache/fop/util/LanguageTags.java
@@ -0,0 +1,66 @@
+/*
+ * 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.util;
+
+import java.util.Locale;
+
+/**
+ * Provides utility methods for manipulating language tags compliant with the
+ * RFC 3066 specification available at http://www.ietf.org/rfc/rfc3066.txt. A
+ * typical language tag is a 2-letter language code sometimes followed by a country
+ * code. For example: en, en-US.
+ */
+public final class LanguageTags {
+
+    private LanguageTags() {
+    }
+
+    /**
+     * Converts the given locale to an RFC 3066 compliant language tag.
+     *
+     * @param locale a locale
+     * @return the corresponding language tag
+     * @throws NullPointerException if the specified locale is null
+     */
+    public static String toLanguageTag(Locale locale) {
+        StringBuffer sb = new StringBuffer(5);
+        sb.append(locale.getLanguage());
+        String country = locale.getCountry();
+        if (country.length() > 0) {
+            sb.append('-');
+            sb.append(country);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Converts an RFC 3066 compliant language tag to a locale.
+     *
+     * @throws NullPointerException if the specified language tag is null
+     */
+    public static Locale toLocale(String languageTag) {
+        String[] parts = languageTag.split("-");
+        if (parts.length == 1) {
+            return new Locale(parts[0]);
+        } else {
+            return new Locale(parts[0], parts[1]);
+        }
+    }
+}
diff --git a/src/java/org/apache/fop/util/XMLUtil.java b/src/java/org/apache/fop/util/XMLUtil.java
index 0a55ce573..d4397c2c7 100644
--- a/src/java/org/apache/fop/util/XMLUtil.java
+++ b/src/java/org/apache/fop/util/XMLUtil.java
@@ -21,7 +21,6 @@ package org.apache.fop.util;
 
 import java.awt.Rectangle;
 import java.awt.geom.Rectangle2D;
-import java.util.Locale;
 
 import org.xml.sax.Attributes;
 import org.xml.sax.SAXException;
@@ -174,39 +173,4 @@ public final class XMLUtil implements XMLConstants {
         atts.addAttribute("", localName, localName, XMLUtil.CDATA, value);
     }
 
-    /**
-     * Converts a {@link Locale} instance to an RFC 3066 compliant language identifier.
-     * @param language the language
-     * @return the formatted language identifier
-     */
-    public static String toRFC3066(Locale language) {
-        if (language == null || language.getLanguage().length() == 0) {
-            return null;
-        }
-        StringBuffer sb = new StringBuffer();
-        sb.append(language.getLanguage());
-        if (language.getCountry().length() > 0) {
-            sb.append('-');
-            sb.append(language.getCountry());
-        }
-        return sb.toString();
-    }
-
-    /**
-     * Converts an RFC 3066 compliant language identifier to a {@link Locale} instance.
-     * @param lang the language string
-     * @return the converted locale instance
-     */
-    public static Locale convertRFC3066ToLocale(String lang) {
-        if (lang == null || lang.length() == 0) {
-            return null;
-        }
-        String[] parts = lang.split("-");
-        if (parts.length == 1) {
-            return new Locale(parts[0]);
-        } else {
-            return new Locale(parts[0], parts[1]);
-        }
-    }
-
 }
diff --git a/test/accessibility/config-painter.xconf b/test/accessibility/config-painter.xconf
deleted file mode 100644
index 8c5dc2bd5..000000000
--- a/test/accessibility/config-painter.xconf
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-  true
-  144
-  false
-  ../resources/fonts/
-  
-    
-      
-        null
-      
-      
-        flate
-        ascii-85
-      
-      
-        
-          
-        
-      
-    
-  
-
diff --git a/test/accessibility/config-renderer.xconf b/test/accessibility/config-renderer.xconf
deleted file mode 100644
index 4b55e393f..000000000
--- a/test/accessibility/config-renderer.xconf
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-  true
-  true
-  144
-  false
-  ../resources/fonts/
-  
-    
-      
-        null
-      
-      
-        flate
-        ascii-85
-      
-      
-        
-          
-        
-      
-    
-  
-
diff --git a/test/accessibility/fop.xconf b/test/accessibility/fop.xconf
new file mode 100644
index 000000000..8c5dc2bd5
--- /dev/null
+++ b/test/accessibility/fop.xconf
@@ -0,0 +1,23 @@
+
+
+  true
+  144
+  false
+  ../resources/fonts/
+  
+    
+      
+        null
+      
+      
+        flate
+        ascii-85
+      
+      
+        
+          
+        
+      
+    
+  
+
diff --git a/test/accessibility/pdf/background-image_jpg_repeat.pdf b/test/accessibility/pdf/background-image_jpg_repeat.pdf
index c7c27f9d1..97b304c53 100644
Binary files a/test/accessibility/pdf/background-image_jpg_repeat.pdf and b/test/accessibility/pdf/background-image_jpg_repeat.pdf differ
diff --git a/test/accessibility/pdf/background-image_jpg_single.pdf b/test/accessibility/pdf/background-image_jpg_single.pdf
index 080f0b876..e1d7f9340 100644
Binary files a/test/accessibility/pdf/background-image_jpg_single.pdf and b/test/accessibility/pdf/background-image_jpg_single.pdf differ
diff --git a/test/accessibility/pdf/background-image_png_repeat.pdf b/test/accessibility/pdf/background-image_png_repeat.pdf
index a10e1357e..9f5e7ff12 100644
Binary files a/test/accessibility/pdf/background-image_png_repeat.pdf and b/test/accessibility/pdf/background-image_png_repeat.pdf differ
diff --git a/test/accessibility/pdf/background-image_png_single.pdf b/test/accessibility/pdf/background-image_png_single.pdf
index 1e9ff00b4..81156e18d 100644
Binary files a/test/accessibility/pdf/background-image_png_single.pdf and b/test/accessibility/pdf/background-image_png_single.pdf differ
diff --git a/test/accessibility/pdf/background-image_svg_repeat.pdf b/test/accessibility/pdf/background-image_svg_repeat.pdf
index c641fbbe5..0e438958d 100644
Binary files a/test/accessibility/pdf/background-image_svg_repeat.pdf and b/test/accessibility/pdf/background-image_svg_repeat.pdf differ
diff --git a/test/accessibility/pdf/background-image_svg_single.pdf b/test/accessibility/pdf/background-image_svg_single.pdf
index 09a9ad895..a46b6df38 100644
Binary files a/test/accessibility/pdf/background-image_svg_single.pdf and b/test/accessibility/pdf/background-image_svg_single.pdf differ
diff --git a/test/accessibility/pdf/complete.pdf b/test/accessibility/pdf/complete.pdf
index caf7a41f8..a42bb2a97 100644
Binary files a/test/accessibility/pdf/complete.pdf and b/test/accessibility/pdf/complete.pdf differ
diff --git a/test/accessibility/pdf/image_jpg.pdf b/test/accessibility/pdf/image_jpg.pdf
index 1336f99eb..6b70c268d 100644
Binary files a/test/accessibility/pdf/image_jpg.pdf and b/test/accessibility/pdf/image_jpg.pdf differ
diff --git a/test/accessibility/pdf/image_png.pdf b/test/accessibility/pdf/image_png.pdf
index b22c157b0..d63db6708 100644
Binary files a/test/accessibility/pdf/image_png.pdf and b/test/accessibility/pdf/image_png.pdf differ
diff --git a/test/accessibility/pdf/image_svg.pdf b/test/accessibility/pdf/image_svg.pdf
index 1f5393c43..94720f564 100644
Binary files a/test/accessibility/pdf/image_svg.pdf and b/test/accessibility/pdf/image_svg.pdf differ
diff --git a/test/accessibility/pdf/image_wmf.pdf b/test/accessibility/pdf/image_wmf.pdf
index 599b4c5ff..4b6eb0827 100644
Binary files a/test/accessibility/pdf/image_wmf.pdf and b/test/accessibility/pdf/image_wmf.pdf differ
diff --git a/test/accessibility/pdf/leader.pdf b/test/accessibility/pdf/leader.pdf
index cf77a5c96..4a6c25253 100644
Binary files a/test/accessibility/pdf/leader.pdf and b/test/accessibility/pdf/leader.pdf differ
diff --git a/test/accessibility/pdf/links.pdf b/test/accessibility/pdf/links.pdf
index ee37c72d6..b8d4e3be3 100644
Binary files a/test/accessibility/pdf/links.pdf and b/test/accessibility/pdf/links.pdf differ
diff --git a/test/accessibility/pdf/role.pdf b/test/accessibility/pdf/role.pdf
index 1eaaf1a9d..ec5d5f536 100644
Binary files a/test/accessibility/pdf/role.pdf and b/test/accessibility/pdf/role.pdf differ
diff --git a/test/accessibility/pdf/role_non-standard.pdf b/test/accessibility/pdf/role_non-standard.pdf
index f8b1c9d2c..d90693066 100644
Binary files a/test/accessibility/pdf/role_non-standard.pdf and b/test/accessibility/pdf/role_non-standard.pdf differ
diff --git a/test/accessibility/pdf/text_1.pdf b/test/accessibility/pdf/text_1.pdf
index 2ad8a12f2..0b1e1ee9f 100644
Binary files a/test/accessibility/pdf/text_1.pdf and b/test/accessibility/pdf/text_1.pdf differ
diff --git a/test/accessibility/pdf/text_2.pdf b/test/accessibility/pdf/text_2.pdf
index 21ad6d0d8..3330cc902 100644
Binary files a/test/accessibility/pdf/text_2.pdf and b/test/accessibility/pdf/text_2.pdf differ
diff --git a/test/accessibility/pdf/text_font-embedding.pdf b/test/accessibility/pdf/text_font-embedding.pdf
index d5c61c040..a268dbb63 100644
Binary files a/test/accessibility/pdf/text_font-embedding.pdf and b/test/accessibility/pdf/text_font-embedding.pdf differ
diff --git a/test/java/org/apache/fop/StandardTestSuite.java b/test/java/org/apache/fop/StandardTestSuite.java
index 1dacd9614..840b04917 100644
--- a/test/java/org/apache/fop/StandardTestSuite.java
+++ b/test/java/org/apache/fop/StandardTestSuite.java
@@ -68,7 +68,8 @@ import org.apache.fop.traits.MinOptMaxTest;
     CharactersetEncoderTest.class,
     org.apache.fop.render.afp.AFPTestSuite.class,
     PSTestSuite.class,
-    MinOptMaxTest.class
+    MinOptMaxTest.class,
+    org.apache.fop.render.intermediate.IFStructureTreeBuilderTestCase.class
 })
 public class StandardTestSuite {
 }
diff --git a/test/java/org/apache/fop/accessibility/FO2StructureTreeConverterTestCase.java b/test/java/org/apache/fop/accessibility/FO2StructureTreeConverterTestCase.java
new file mode 100644
index 000000000..f8719edd6
--- /dev/null
+++ b/test/java/org/apache/fop/accessibility/FO2StructureTreeConverterTestCase.java
@@ -0,0 +1,202 @@
+/*
+ * 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.accessibility;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.TransformerFactoryConfigurationError;
+import javax.xml.transform.dom.DOMResult;
+import javax.xml.transform.sax.SAXTransformerFactory;
+import javax.xml.transform.sax.TransformerHandler;
+import javax.xml.transform.stream.StreamSource;
+
+import org.custommonkey.xmlunit.Diff;
+import org.custommonkey.xmlunit.Difference;
+import org.custommonkey.xmlunit.DifferenceConstants;
+import org.custommonkey.xmlunit.DifferenceListener;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
+
+import org.apache.fop.apps.FOPException;
+import org.apache.fop.apps.FOUserAgent;
+import org.apache.fop.fo.FODocumentParser;
+import org.apache.fop.fo.FODocumentParser.FOEventHandlerFactory;
+import org.apache.fop.fo.FOEventHandler;
+import org.apache.fop.fo.LoadingException;
+import org.apache.fop.fotreetest.DummyFOEventHandler;
+
+public class FO2StructureTreeConverterTestCase {
+
+    private static class IgnoringPtrDifferenceListener implements DifferenceListener {
+
+        public int differenceFound(Difference difference) {
+            switch (difference.getId()) {
+            case DifferenceConstants.ELEMENT_NUM_ATTRIBUTES_ID:
+                return RETURN_IGNORE_DIFFERENCE_NODES_SIMILAR;
+            case DifferenceConstants.ATTR_NAME_NOT_FOUND_ID:
+                String additionalAttribute = difference.getTestNodeDetail().getValue();
+                if (additionalAttribute.equals("ptr")) {
+                    return RETURN_IGNORE_DIFFERENCE_NODES_SIMILAR;
+                } else {
+                    return RETURN_ACCEPT_DIFFERENCE;
+                }
+            default:
+                return RETURN_ACCEPT_DIFFERENCE;
+            }
+        }
+
+        public void skippedComparison(Node control, Node test) {
+            throw new UnsupportedOperationException("Not implemented");
+        }
+    }
+
+    private static final String STRUCTURE_TREE_SEQUENCE_NAME = "structure-tree-sequence";
+
+    @Test
+    public void testConverter() throws Exception {
+        DOMResult expectedStructureTree = loadExpectedStructureTree();
+        DOMResult actualStructureTree = buildActualStructureTree();
+        final Diff diff = createDiff(expectedStructureTree, actualStructureTree);
+        assertTrue(diff.toString(), diff.similar());
+    }
+
+    private static DOMResult loadExpectedStructureTree() {
+        DOMResult expectedStructureTree = new DOMResult();
+        runXSLT(getXsltInputStream(), getFoInputStream(), expectedStructureTree);
+        return expectedStructureTree;
+    }
+
+    private static InputStream getXsltInputStream() {
+        return FO2StructureTreeConverterTestCase.class.getResourceAsStream("foToIfStructureTree.xsl");
+    }
+
+    private static InputStream getFoInputStream() {
+        return FO2StructureTreeConverterTestCase.class.getResourceAsStream(
+                "/org/apache/fop/fo/complete_document.fo");
+    }
+
+    private static void runXSLT(InputStream xslt, InputStream doc, Result result) {
+        Source fo = new StreamSource(doc);
+        try {
+            Transformer transformer = TransformerFactory.newInstance()
+                    .newTransformer(new StreamSource(xslt));
+            transformer.transform(fo, result);
+        } catch (TransformerConfigurationException e) {
+            throw new RuntimeException(e);
+        } catch (TransformerException e) {
+            throw new RuntimeException(e);
+        } finally {
+            closeStream(xslt);
+            closeStream(doc);
+        }
+    }
+
+    private static void closeStream(InputStream stream) {
+        try {
+            stream.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static DOMResult buildActualStructureTree() throws Exception {
+        DOMResult actualStructureTree = new DOMResult();
+        createStructureTreeFromDocument(getFoInputStream(), actualStructureTree);
+        return actualStructureTree;
+    }
+
+    private static void createStructureTreeFromDocument(InputStream foInputStream,
+            DOMResult domResult) throws Exception {
+        TransformerHandler tHandler = createTransformerHandler(domResult);
+        startStructureTreeSequence(tHandler);
+        StructureTreeEventHandler structureTreeEventHandler
+                = StructureTree2SAXEventAdapter.newInstance(tHandler);
+        FODocumentParser documentParser = createDocumentParser(structureTreeEventHandler);
+        FOUserAgent userAgent = createFOUserAgent(documentParser);
+        parseDocument(foInputStream, documentParser, userAgent);
+        endStructureTreeSequence(tHandler);
+    }
+
+    private static TransformerHandler createTransformerHandler(DOMResult domResult)
+            throws TransformerConfigurationException, TransformerFactoryConfigurationError {
+        SAXTransformerFactory factory = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
+        TransformerHandler transformerHandler = factory.newTransformerHandler();
+        transformerHandler.setResult(domResult);
+        return transformerHandler;
+    }
+
+    private static void startStructureTreeSequence(TransformerHandler tHandler) throws SAXException {
+        tHandler.startDocument();
+        tHandler.startElement("", STRUCTURE_TREE_SEQUENCE_NAME, STRUCTURE_TREE_SEQUENCE_NAME,
+                new AttributesImpl());
+    }
+
+    private static FODocumentParser createDocumentParser(
+            final StructureTreeEventHandler structureTreeEventHandler) {
+        return FODocumentParser.newInstance(new FOEventHandlerFactory() {
+            public FOEventHandler newFOEventHandler(FOUserAgent foUserAgent) {
+                return new FO2StructureTreeConverter(structureTreeEventHandler,
+                        new DummyFOEventHandler(foUserAgent));
+            }
+        });
+    }
+
+    private static FOUserAgent createFOUserAgent(FODocumentParser documentParser) {
+        FOUserAgent userAgent = documentParser.createFOUserAgent();
+        userAgent.setAccessibility(true);
+        return userAgent;
+    }
+
+    private static void parseDocument(InputStream foInputStream, FODocumentParser documentParser,
+            FOUserAgent userAgent) throws FOPException, LoadingException {
+        try {
+            documentParser.parse(foInputStream, userAgent);
+        } finally {
+            closeStream(foInputStream);
+        }
+    }
+
+    private static void endStructureTreeSequence(TransformerHandler tHandler) throws SAXException {
+        tHandler.endElement("", STRUCTURE_TREE_SEQUENCE_NAME, STRUCTURE_TREE_SEQUENCE_NAME);
+        tHandler.endDocument();
+    }
+
+    private static Diff createDiff(DOMResult expected, DOMResult actual) {
+        Diff diff = new Diff(getDocument(expected), getDocument(actual));
+        diff.overrideDifferenceListener(new IgnoringPtrDifferenceListener());
+        return diff;
+    }
+
+    private static Document getDocument(DOMResult result) {
+        return (Document) result.getNode();
+    }
+}
diff --git a/test/java/org/apache/fop/accessibility/fo2StructureTree.xsl b/test/java/org/apache/fop/accessibility/fo2StructureTree.xsl
new file mode 100644
index 000000000..5c9c5615c
--- /dev/null
+++ b/test/java/org/apache/fop/accessibility/fo2StructureTree.xsl
@@ -0,0 +1,116 @@
+
+
+
+
+
+  
+
+  
+    
+      
+    
+  
+
+
+  
+  
+    
+      
+    
+  
+
+  
+  
+    
+        
+    
+  
+
+
+  
+  
+    
+  
+
+  
+  
+    
+  
+
+  
+  
+    
+  
+
+  
+    
+  
+
+  
+    
+  
+
+  
+  
+    
+  
+
+  
+    
+  
+
+  
+  
+    
+  
+
+  
+  
+    
+  
+
+  
+  
+    
+  
+
+  
+  
+    
+  
+
+
+  
+  
+
+
+  
+  
+    
+  
+
+  
+
+
+  
+  
+
+
diff --git a/test/java/org/apache/fop/fo/DelegatingFOEventHandlerTestCase.fo b/test/java/org/apache/fop/fo/DelegatingFOEventHandlerTestCase.fo
deleted file mode 100644
index 74afd6c16..000000000
--- a/test/java/org/apache/fop/fo/DelegatingFOEventHandlerTestCase.fo
+++ /dev/null
@@ -1,175 +0,0 @@
-
-
-
-
-
-  
-    
-      
-      
-    
-  
-
-  
-    
-      This is the page headerPage 
-    
-    
-      
-    
-    
-      This is a link to the next page-sequence 
-        (which starts on page  and ends on 
-        page ).
-      A Title Block
-      This block of text contains a footnote1A footnote with a link to the  FOP 
-                  website 
-        call.
-      
-        
-        
-        
-          
-            
-              Header 1.1
-            
-            
-              Header 1.2
-            
-          
-        
-        
-          
-            
-              Footer 1.1
-            
-            
-              Footer 1.2
-            
-          
-        
-        
-          
-            
-              Cell 1.1
-            
-            
-              Cell 1.2
-            
-          
-          
-            
-              Cell 2.1
-            
-            
-              Cell 2.2
-            
-          
-        
-      
-      
-        
-          This is an absolutely positioned 
-            block-container. Nullam interdum mattis ipsum sit amet molestie.
-        
-        Lorem ipsum dolor sit amet, consectetur adipiscing 
-          elit. Integer vel lacinia diam. Etiam venenatis magna vel libero imperdiet 
-          rhoncus.
-      
-    
-  
-
-  
-    
-      This is the page headerPage 
-        
-    
-    
-      Starting a new page-sequence.
-      The  
-        logo.
-      
-        
-          
-            1.
-          
-          
-            First item of a list
-          
-        
-        
-          
-            2.
-          
-          
-            Second item of a list
-          
-        
-        
-          
-            3.
-          
-          
-            Third item of a list
-          
-        
-      
-      
-          
-            
-              
-              
-              
-            
-          
-      
-      A block containing an inline 
-        element.
-      A block containing a fancy  
-        character.
-      A leader with special 
-        content: .
-      Ending the page-sequence.
-    
-  
-
-
diff --git a/test/java/org/apache/fop/fo/DelegatingFOEventHandlerTestCase.java b/test/java/org/apache/fop/fo/DelegatingFOEventHandlerTestCase.java
index faa88e38e..313379e02 100644
--- a/test/java/org/apache/fop/fo/DelegatingFOEventHandlerTestCase.java
+++ b/test/java/org/apache/fop/fo/DelegatingFOEventHandlerTestCase.java
@@ -71,6 +71,7 @@ import org.apache.fop.fo.flow.table.TableHeader;
 import org.apache.fop.fo.flow.table.TableRow;
 import org.apache.fop.fo.pagination.Flow;
 import org.apache.fop.fo.pagination.PageSequence;
+import org.apache.fop.fo.pagination.Root;
 import org.apache.fop.fo.pagination.StaticContent;
 
 /**
@@ -97,12 +98,22 @@ public class DelegatingFOEventHandlerTestCase {
 
         @Override
         public void startDocument() throws SAXException {
-            actualEvents.add("start root");
+            actualEvents.add("start document");
         }
 
         @Override
         public void endDocument() throws SAXException {
-            actualEvents.add("end   root");
+            actualEvents.add("end   document");
+        }
+
+        @Override
+        public void startRoot(Root root) {
+            startElement(root);
+        }
+
+        @Override
+        public void endRoot(Root root) {
+            endElement(root);
         }
 
         @Override
@@ -417,9 +428,7 @@ public class DelegatingFOEventHandlerTestCase {
     }
 
     private void loadDocument() {
-        Class clazz = getClass();
-        String documentName = clazz.getSimpleName() + ".fo";
-        document = clazz.getResourceAsStream(documentName);
+        document = getClass().getResourceAsStream("complete_document.fo");
     }
 
     private void loadExpectedEvents() throws IOException {
diff --git a/test/java/org/apache/fop/fo/FODocumentParser.java b/test/java/org/apache/fop/fo/FODocumentParser.java
index 4706866a4..a7574e49d 100644
--- a/test/java/org/apache/fop/fo/FODocumentParser.java
+++ b/test/java/org/apache/fop/fo/FODocumentParser.java
@@ -108,13 +108,29 @@ public final class FODocumentParser {
      * @throws LoadingException if an error occurs when parsing the document
      */
     public void parse(InputStream document) throws FOPException, LoadingException {
-        FOUserAgent foUserAgent = createFOUserAgent();
+        parse(document, createFOUserAgent());
+    }
+
+    /**
+     * Runs FOP on the given document with the supplied {@link FOUserAgent}.
+     *
+     * @param document XSL-FO document to parse
+     * @param foUserAgent The user agent
+     * @throws FOPException if an error occurs when initializing FOP
+     * @throws LoadingException if an error occurs when parsing the document
+     */
+    public void parse(InputStream document, FOUserAgent foUserAgent)
+            throws FOPException, LoadingException {
         fop = FOP_FACTORY.newFop(foUserAgent);
         createTransformer();
         runTransformer(document);
     }
 
-    private FOUserAgent createFOUserAgent() {
+    /**
+     * Creates a new {@link FOUserAgent}.
+     * @return It
+     */
+    public FOUserAgent createFOUserAgent() {
         FOUserAgent userAgent = FOP_FACTORY.newFOUserAgent();
         FOEventHandler foEventHandler = foEventHandlerFactory.newFOEventHandler(userAgent);
         userAgent.setFOEventHandlerOverride(foEventHandler);
diff --git a/test/java/org/apache/fop/fo/complete_document.fo b/test/java/org/apache/fop/fo/complete_document.fo
new file mode 100644
index 000000000..5a34e9e9a
--- /dev/null
+++ b/test/java/org/apache/fop/fo/complete_document.fo
@@ -0,0 +1,176 @@
+
+
+
+
+
+  
+    
+      
+      
+    
+  
+
+  
+    
+      This is the page headerPage 
+    
+    
+      
+    
+    
+      This is a link to the next page-sequence 
+        (which starts on page  and ends on 
+        page ).
+      A Title Block
+      This block of text contains a footnote1A footnote with a link to the  FOP 
+                  website 
+        call.
+      
+        
+        
+        
+          
+            
+              Header 1.1
+            
+            
+              Header 1.2
+            
+          
+        
+        
+          
+            
+              Footer 1.1
+            
+            
+              Footer 1.2
+            
+          
+        
+        
+          
+            
+              Cell 1.1
+            
+            
+              Cell 1.2
+            
+          
+          
+            
+              Cell 2.1
+            
+            
+              Cell 2.2
+            
+          
+        
+      
+      
+        
+          This is an absolutely positioned 
+            block-container. Nullam interdum mattis ipsum sit amet molestie.
+        
+        Lorem ipsum dolor sit amet, consectetur adipiscing 
+          elit. Integer vel lacinia diam. Etiam venenatis magna vel libero imperdiet 
+          rhoncus.
+      
+    
+  
+
+  
+    
+      This is the page headerPage 
+        
+    
+    
+      Starting a new page-sequence.
+      The  
+        logo.
+      
+        
+          
+            1.
+          
+          
+            First item of a list
+          
+        
+        
+          
+            2.
+          
+          
+            Second item of a list
+          
+        
+        
+          
+            3.
+          
+          
+            Third item of a list
+          
+        
+      
+      
+          
+            
+              
+              
+              
+            
+          
+      
+      A block containing an inline 
+        element.
+      A block containing a fancy  
+        character.
+      A leader with special 
+        content: .
+      Ending the page-sequence.
+    
+  
+
+
diff --git a/test/java/org/apache/fop/fo/extract-events.xsl b/test/java/org/apache/fop/fo/extract-events.xsl
index 8edc0ee49..6cf42c984 100644
--- a/test/java/org/apache/fop/fo/extract-events.xsl
+++ b/test/java/org/apache/fop/fo/extract-events.xsl
@@ -6,16 +6,22 @@
 
   
 
-  
+  
     
-      start root
+      start document
     
-    
+    
     
-      end   root
+      end   document
     
   
 
+  
+    start root
+    
+    end   root
+  
+
   
     
       
diff --git a/test/java/org/apache/fop/fo/properties/CommonAccessibilityHolderTestCase.java b/test/java/org/apache/fop/fo/properties/CommonAccessibilityHolderTestCase.java
index 45f6a61ab..352a39713 100644
--- a/test/java/org/apache/fop/fo/properties/CommonAccessibilityHolderTestCase.java
+++ b/test/java/org/apache/fop/fo/properties/CommonAccessibilityHolderTestCase.java
@@ -34,6 +34,7 @@ import org.apache.fop.fo.FONode;
 import org.apache.fop.fo.FONodeMocks;
 import org.apache.fop.fo.PropertyList;
 import org.apache.fop.fo.expr.PropertyException;
+import org.apache.fop.fo.flow.table.UnimplementedWarningNeutralizer;
 
 /**
  * This tests that all the FONodes that implement CommonAccessibilityHolder correctly configure
@@ -51,7 +52,7 @@ public class CommonAccessibilityHolderTestCase {
     static {
         /* This triggers 'unimplemented feature' FO validation events so that the event system is
          * not triggered when testing, avoiding extra convoluted dependency stubbing. */
-//        UnimplementedWarningNeutralizer.neutralizeUnimplementedWarning();
+        UnimplementedWarningNeutralizer.neutralizeUnimplementedWarning();
 
         IMPLEMENTATIONS.add(org.apache.fop.fo.flow.BasicLink.class);
         IMPLEMENTATIONS.add(org.apache.fop.fo.flow.Block.class);
diff --git a/test/java/org/apache/fop/intermediate/IFParserTestCase.java b/test/java/org/apache/fop/intermediate/IFParserTestCase.java
index 52d322e3e..6ac20c556 100644
--- a/test/java/org/apache/fop/intermediate/IFParserTestCase.java
+++ b/test/java/org/apache/fop/intermediate/IFParserTestCase.java
@@ -48,6 +48,9 @@ import org.apache.fop.render.intermediate.IFSerializer;
 @RunWith(Parameterized.class)
 public class IFParserTestCase extends AbstractIFTestCase {
 
+    /** Set this to true to get the correspondence between test number and test file. */
+    private static final boolean DEBUG = false;
+
     /**
      * Gets the parameters for this test
      *
@@ -56,7 +59,19 @@ public class IFParserTestCase extends AbstractIFTestCase {
      */
     @Parameters
     public static Collection getParameters() throws IOException {
-        return LayoutEngineTestUtils.getLayoutTestFiles();
+        Collection testFiles = LayoutEngineTestUtils.getLayoutTestFiles();
+        if (DEBUG) {
+            printFiles(testFiles);
+        }
+        return testFiles;
+    }
+
+    private static void printFiles(Collection files) {
+        int index = 0;
+        for (File[] file : files) {
+            assert file.length == 1;
+            System.out.println(String.format("%3d %s", index++, file[0]));
+        }
     }
 
     /**
diff --git a/test/java/org/apache/fop/render/intermediate/IFStructureTreeBuilderTestCase.java b/test/java/org/apache/fop/render/intermediate/IFStructureTreeBuilderTestCase.java
new file mode 100644
index 000000000..d7db7db7f
--- /dev/null
+++ b/test/java/org/apache/fop/render/intermediate/IFStructureTreeBuilderTestCase.java
@@ -0,0 +1,169 @@
+/*
+ * 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.render.intermediate;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentMatcher;
+import org.mockito.InOrder;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
+
+import org.apache.fop.fo.FOElementMapping;
+import org.apache.fop.fo.extensions.ExtensionElementMapping;
+import org.apache.fop.fo.extensions.InternalElementMapping;
+import org.apache.fop.util.XMLUtil;
+
+public class IFStructureTreeBuilderTestCase {
+
+    private IFStructureTreeBuilder sut;
+
+    @Before
+    public void setUp() {
+        sut = new IFStructureTreeBuilder();
+    }
+
+    @Test
+    public void startAndEndPageSequence() throws SAXException {
+        final ContentHandler handler = mock(ContentHandler.class);
+
+        try {
+            sut.replayEventsForPageSequence(handler, 0);
+            fail("No page sequences created");
+        } catch (IndexOutOfBoundsException e) {
+            // Expected
+        }
+
+        sut.startPageSequence(null);
+        sut.endPageSequence();
+
+        sut.replayEventsForPageSequence(handler, 0);
+
+        InOrder inOrder = inOrder(handler);
+
+        inOrder.verify(handler).startPrefixMapping(
+                InternalElementMapping.STANDARD_PREFIX, InternalElementMapping.URI);
+        inOrder.verify(handler).startPrefixMapping(
+                ExtensionElementMapping.STANDARD_PREFIX, ExtensionElementMapping.URI);
+        inOrder.verify(handler).startElement(eq(IFConstants.NAMESPACE),
+                eq(IFConstants.EL_STRUCTURE_TREE),
+                eq(IFConstants.EL_STRUCTURE_TREE),
+                any(Attributes.class));
+        inOrder.verify(handler).endElement(eq(IFConstants.NAMESPACE),
+                eq(IFConstants.EL_STRUCTURE_TREE),
+                eq(IFConstants.EL_STRUCTURE_TREE));
+        inOrder.verify(handler).endPrefixMapping(ExtensionElementMapping.STANDARD_PREFIX);
+        inOrder.verify(handler).endPrefixMapping(InternalElementMapping.STANDARD_PREFIX);
+    }
+
+    @Test
+    public void startNode() throws Exception {
+        final String[] attributes = {"ptr", "1"};
+        final String nodeName = "block";
+        final ContentHandler handler = mock(ContentHandler.class);
+
+        sut.startPageSequence(null);
+        sut.startNode(nodeName, createSimpleAttributes(attributes));
+        sut.endPageSequence();
+
+        sut.replayEventsForPageSequence(handler, 0);
+
+        verify(handler).startElement(eq(FOElementMapping.URI), eq(nodeName),
+                eq(FOElementMapping.STANDARD_PREFIX + ":" + nodeName),
+                AttributesMatcher.match(createSimpleAttributes(attributes)));
+    }
+
+    @Test
+    public void endNode() throws Exception {
+        final String nodeName = "block";
+        final ContentHandler handler = mock(ContentHandler.class);
+
+        sut.startPageSequence(null);
+        sut.endNode(nodeName);
+        sut.endPageSequence();
+
+        sut.replayEventsForPageSequence(handler, 0);
+
+        verify(handler).endElement(eq(FOElementMapping.URI), eq(nodeName),
+                eq(FOElementMapping.STANDARD_PREFIX + ":" + nodeName));
+    }
+
+    private static Attributes createSimpleAttributes(String... attributes) {
+        assert (attributes.length % 2 == 0);
+        final AttributesImpl atts = new AttributesImpl();
+        for (int i = 0; i < attributes.length; i += 2) {
+            String key = attributes[i];
+            String value = attributes[i + 1];
+            atts.addAttribute("", key, key, XMLUtil.CDATA, value);
+        }
+        return atts;
+    }
+
+    private static final class AttributesMatcher extends ArgumentMatcher {
+
+        private final Attributes expected;
+
+        private AttributesMatcher(Attributes expected) {
+            this.expected = expected;
+        }
+
+        public static Attributes match(Attributes expected) {
+            return argThat(new AttributesMatcher(expected));
+        }
+
+        public boolean matches(Object attributes) {
+            return attributesEqual(expected, (Attributes) attributes);
+        }
+
+        private static boolean attributesEqual(Attributes attributes1, Attributes attributes2) {
+            if (attributes1.getLength() != attributes2.getLength()) {
+                return false;
+            }
+            for (int i = 0; i < attributes1.getLength(); i++) {
+                if (attributes1.getLocalName(i) != attributes2.getLocalName(i)) {
+                    return false;
+                }
+                if (attributes1.getQName(i) != attributes2.getQName(i)) {
+                    return false;
+                }
+                if (attributes1.getType(i) != attributes2.getType(i)) {
+                    return false;
+                }
+                if (attributes1.getURI(i) != attributes2.getURI(i)) {
+                    return false;
+                }
+                if (attributes1.getValue(i) != attributes2.getValue(i)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+}
diff --git a/test/java/org/apache/fop/render/intermediate/SAXEventRecorderTestCase.java b/test/java/org/apache/fop/render/intermediate/SAXEventRecorderTestCase.java
new file mode 100644
index 000000000..c5aad66d0
--- /dev/null
+++ b/test/java/org/apache/fop/render/intermediate/SAXEventRecorderTestCase.java
@@ -0,0 +1,131 @@
+/*
+ * 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.render.intermediate;
+
+
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
+
+import org.apache.fop.render.intermediate.IFStructureTreeBuilder.SAXEventRecorder;
+import org.apache.fop.util.XMLUtil;
+
+/**
+ * Tests {@link SAXEventRecorder}.
+ */
+public class SAXEventRecorderTestCase {
+
+    private static final String URI = "http://www.example.com/";
+
+    private SAXEventRecorder sut;
+
+    @Before
+    public void setUp() {
+        sut = new SAXEventRecorder();
+    }
+
+    @Test
+    public void testStartEvent() throws SAXException {
+        final String localName = "element";
+        final String qName = "prefix:" + localName;
+        final Attributes attributes = new AttributesImpl();
+
+        sut.startElement(URI, localName, qName, attributes);
+        ContentHandler handler = mock(ContentHandler.class);
+        sut.replay(handler);
+        verify(handler).startElement(URI, localName, qName, attributes);
+    }
+
+    @Test
+    public void testEndEvent() throws SAXException {
+        final String localName = "element";
+        final String qName = "prefix:" + localName;
+        sut.endElement(URI, localName, qName);
+        ContentHandler handler = mock(ContentHandler.class);
+        sut.replay(handler);
+        verify(handler).endElement(URI, localName, qName);
+    }
+
+    @Test
+    public void testStartPrefixMapping() throws SAXException {
+        final String prefix = "prefix";
+
+        sut.startPrefixMapping(URI, prefix);
+        ContentHandler handler = mock(ContentHandler.class);
+        sut.replay(handler);
+        verify(handler).startPrefixMapping(URI, prefix);
+    }
+
+    @Test
+    public void testEndPrefixMapping() throws SAXException {
+        final String prefix = "prefix";
+
+        sut.endPrefixMapping(prefix);
+        ContentHandler handler = mock(ContentHandler.class);
+        sut.replay(handler);
+        verify(handler).endPrefixMapping(prefix);
+    }
+
+    @Test
+    public void completeTest() throws SAXException {
+        final String localName1 = "element";
+        final String qName1 = "prefix:" + localName1;
+        final Attributes attributes1 = createAttributes(URI, localName1, qName1, "value-1");
+        final String localName2 = "element2";
+        final String qName2 = "prefix:" + localName2;
+        final Attributes attributes2 = createAttributes(URI, localName2, qName2, "value-2");
+        final ContentHandler handler = mock(ContentHandler.class);
+        final String extensionUrl = "http://www.example.com/extension";
+        final String extensionPrefix = "ext";
+
+        sut.startPrefixMapping(extensionPrefix, extensionUrl);
+        sut.startElement(URI, localName1, qName1, attributes1);
+        sut.startElement(URI, localName2, qName2, attributes2);
+        sut.endElement(URI, localName2, qName2);
+        sut.endElement(URI, localName1, qName1);
+        sut.endPrefixMapping(extensionPrefix);
+
+        sut.replay(handler);
+
+        InOrder inOrder = inOrder(handler);
+        inOrder.verify(handler).startPrefixMapping(extensionPrefix, extensionUrl);
+        inOrder.verify(handler).startElement(URI, localName1, qName1, attributes1);
+        inOrder.verify(handler).startElement(URI, localName2, qName2, attributes2);
+        inOrder.verify(handler).endElement(URI, localName2, qName2);
+        inOrder.verify(handler).endElement(URI, localName1, qName1);
+        inOrder.verify(handler).endPrefixMapping(extensionPrefix);
+    }
+
+    private static Attributes createAttributes(String uri, String localName,
+            String qName, String value) {
+        final AttributesImpl atts = new AttributesImpl();
+        atts.addAttribute(uri, localName, qName, XMLUtil.CDATA, value);
+        return atts;
+    }
+
+}
diff --git a/test/java/org/apache/fop/util/LanguageTagsTestCase.java b/test/java/org/apache/fop/util/LanguageTagsTestCase.java
new file mode 100644
index 000000000..f7383c720
--- /dev/null
+++ b/test/java/org/apache/fop/util/LanguageTagsTestCase.java
@@ -0,0 +1,58 @@
+/*
+ * 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.util;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Locale;
+
+import org.junit.Test;
+
+/**
+ * Tests {@link LanguageTags}.
+ */
+public class LanguageTagsTestCase {
+
+    @Test(expected = NullPointerException.class)
+    public void toLanguageTagRejectsNull() {
+        LanguageTags.toLanguageTag(null);
+    }
+
+    @Test
+    public void testToLanguageTag() throws Exception {
+        assertEquals("", LanguageTags.toLanguageTag(new Locale("")));
+        assertEquals("en", LanguageTags.toLanguageTag(new Locale("en")));
+        assertEquals("en-US", LanguageTags.toLanguageTag(new Locale("en", "US")));
+        assertEquals("en-US", LanguageTags.toLanguageTag(new Locale("EN", "us")));
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void toLocaleRejectsNull() {
+        LanguageTags.toLocale(null);
+    }
+
+    @Test
+    public void testRFC3066ToLocale() throws Exception {
+        assertEquals(new Locale(""), LanguageTags.toLocale(""));
+        assertEquals(new Locale("en"), LanguageTags.toLocale("en"));
+        assertEquals(new Locale("en", "US"), LanguageTags.toLocale("en-US"));
+        assertEquals(new Locale("en", "US"), LanguageTags.toLocale("EN-us"));
+    }
+}
diff --git a/test/java/org/apache/fop/util/XMLUtilTestCase.java b/test/java/org/apache/fop/util/XMLUtilTestCase.java
deleted file mode 100644
index 4c1b999e1..000000000
--- a/test/java/org/apache/fop/util/XMLUtilTestCase.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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.util;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-
-import java.util.Locale;
-
-import org.junit.Test;
-
-/**
- * Tests {@link XMLUtil}.
- */
-public class XMLUtilTestCase {
-
-    @Test
-    public void testLocaleToRFC3066() throws Exception {
-        assertNull(XMLUtil.toRFC3066(null));
-        assertEquals("en", XMLUtil.toRFC3066(new Locale("en")));
-        assertEquals("en-US", XMLUtil.toRFC3066(new Locale("en", "US")));
-        assertEquals("en-US", XMLUtil.toRFC3066(new Locale("EN", "us")));
-    }
-
-    @Test
-    public void testRFC3066ToLocale() throws Exception {
-        assertNull(XMLUtil.convertRFC3066ToLocale(null));
-        assertNull(XMLUtil.convertRFC3066ToLocale(""));
-        assertEquals(new Locale("en"), XMLUtil.convertRFC3066ToLocale("en"));
-        assertEquals(new Locale("en", "US"), XMLUtil.convertRFC3066ToLocale("en-US"));
-        assertEquals(new Locale("en", "US"), XMLUtil.convertRFC3066ToLocale("EN-us"));
-    }
-}