This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch branch_3x
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/branch_3x by this push:
     new 048d87dd6b TIKA-4938: convert XFAExtractor from StAX to SAX; deprecate 
StAX in XMLReaderUtils (#3267) (#3269)
048d87dd6b is described below

commit 048d87dd6bbd2e855169ce585a26f5090b8def16
Author: Tim Allison <[email protected]>
AuthorDate: Mon Sep 28 16:43:51 2026 -0400

    TIKA-4938: convert XFAExtractor from StAX to SAX; deprecate StAX in 
XMLReaderUtils (#3267) (#3269)
---
 CHANGES.txt                                        |   3 +
 .../java/org/apache/tika/utils/XMLReaderUtils.java |  20 +-
 .../apache/tika/parser/pdf/AbstractPDF2XHTML.java  |   5 +-
 .../java/org/apache/tika/parser/pdf/PDFParser.java |   3 -
 .../org/apache/tika/parser/pdf/XFAExtractor.java   | 401 ++++++++++-----------
 .../apache/tika/parser/pdf/XFAExtractorTest.java   | 124 +++++++
 6 files changed, 330 insertions(+), 226 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 5c68587737..e3bb816566 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,8 @@
 Release 3.3.3 - ???
 
+  * XFAExtractor now parses XFA with SAX instead of StAX;
+    XMLReaderUtils.getXMLInputFactory() is deprecated (TIKA-4938).
+    
   * PDF: a bookmark outline is now walked without recursion, and lists nest at
     most 50 deep with deeper items joining the deepest list (TIKA-4894).
 
diff --git a/tika-core/src/main/java/org/apache/tika/utils/XMLReaderUtils.java 
b/tika-core/src/main/java/org/apache/tika/utils/XMLReaderUtils.java
index fb1f32515c..fe1cd7cae4 100644
--- a/tika-core/src/main/java/org/apache/tika/utils/XMLReaderUtils.java
+++ b/tika-core/src/main/java/org/apache/tika/utils/XMLReaderUtils.java
@@ -294,15 +294,16 @@ public class XMLReaderUtils implements Serializable {
     }
 
     /**
-     * Returns the StAX input factory specified in this parsing context.
-     * If a factory is not explicitly specified, then a default factory
-     * instance is created and returned. The default factory instance is
-     * configured to be namespace-aware and to apply reasonable security
-     * precautions.
+     * Returns a StAX input factory configured to be namespace-aware and to
+     * apply reasonable security precautions.
      *
      * @return StAX input factory
      * @since Apache Tika 1.13
+     * @deprecated since 3.3.3 and 4.2, removal planned for 5.0. Tika no 
longer parses with StAX;
+     * its security settings are best-effort and implementation-dependent. Use
+     * {@link #parseSAX(InputStream, ContentHandler, ParseContext)}.
      */
+    @Deprecated
     public static XMLInputFactory getXMLInputFactory() {
         XMLInputFactory factory = XMLInputFactory.newFactory();
         if (LOG.isDebugEnabled()) {
@@ -1266,14 +1267,13 @@ public class XMLReaderUtils implements Serializable {
     }
 
     /**
-     * Returns the StAX input factory specified in this parsing context.
-     * If a factory is not explicitly specified, then a default factory
-     * instance is created and returned. The default factory instance is
-     * configured to be namespace-aware and to apply reasonable security
-     * precautions.
+     * Returns the StAX input factory in the context, or {@link 
#getXMLInputFactory()}.
      *
      * @return StAX input factory
+     * @deprecated since 3.3.3 and 4.2, removal planned for 5.0; see {@link 
#getXMLInputFactory()}.
+     * Use {@link #parseSAX(InputStream, ContentHandler, ParseContext)}.
      */
+    @Deprecated
     public static XMLInputFactory getXMLInputFactory(ParseContext context) {
         XMLInputFactory factory = context.get(XMLInputFactory.class);
         if (factory != null) {
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java
index 257f269e3e..cd021e85cf 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java
@@ -47,7 +47,6 @@ import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeSet;
-import javax.xml.stream.XMLStreamException;
 
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream;
@@ -1307,8 +1306,8 @@ class AbstractPDF2XHTML extends PDFTextStripper {
                 try {
                     xfaExtractor.extract(is, xhtml, metadata, context);
                     return;
-                } catch (XMLStreamException e) {
-                    //if there was an xml parse exception in xfa, try the 
AcroForm
+                } catch (TikaException e) {
+                    //malformed xfa: record and try the AcroForm
                     EmbeddedDocumentUtil.recordException(e, metadata);
                 } finally {
                     IOUtils.closeQuietly(is);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java
index 3874b7382b..97b56c500c 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java
@@ -28,7 +28,6 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import javax.xml.stream.XMLStreamException;
 
 import org.apache.commons.io.input.CloseShieldInputStream;
 import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream;
@@ -704,8 +703,6 @@ public class PDFParser implements Parser, RenderingParser, 
Initializable {
         try (InputStream is = 
                 
UnsynchronizedByteArrayInputStream.builder().setByteArray(pdDocument.getDocumentCatalog().getAcroForm(null).getXFA().getBytes()).get())
 {
             ex.extract(is, xhtml, metadata, context);
-        } catch (XMLStreamException e) {
-            throw new TikaException("XML error in XFA", e);
         }
         xhtml.endDocument();
     }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/XFAExtractor.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/XFAExtractor.java
index 2ca42620d2..f8f8b6059d 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/XFAExtractor.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/XFAExtractor.java
@@ -16,21 +16,25 @@
  */
 package org.apache.tika.parser.pdf;
 
+import java.io.IOException;
 import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import javax.xml.namespace.QName;
-import javax.xml.stream.XMLStreamConstants;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.stream.XMLStreamReader;
+import java.util.Set;
 
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
 import org.xml.sax.SAXException;
 import org.xml.sax.helpers.AttributesImpl;
+import org.xml.sax.helpers.DefaultHandler;
 
+import org.apache.tika.exception.TikaException;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.parser.ParseContext;
+import org.apache.tika.sax.TaggedContentHandler;
 import org.apache.tika.sax.XHTMLContentHandler;
 import org.apache.tika.utils.XMLReaderUtils;
 
@@ -41,7 +45,6 @@ import org.apache.tika.utils.XMLReaderUtils;
  * <p>
  * Some areas for improvement:
  * <ol>
- *     <li>convert this to 2 lines of XPath</li>
  *     <li>handle metadata stored in &lt;desc&gt; section (govdocs1: 
754282.pdf, 982106.pdf)</li>
  *     <li>handle pdf metadata (access permissions, etc.) in &lt;pdf&gt; 
element</li>
  *     <li>extract different types of uris as metadata</li>
@@ -54,255 +57,233 @@ import org.apache.tika.utils.XMLReaderUtils;
  */
 class XFAExtractor {
 
-    private static final Pattern XFA_TEMPLATE_ANY_VERSION =
-            Pattern.compile("^http://www.xfa.org/schema/xfa-template";);
-    private static final Pattern TEXT_PATTERN =
-            Pattern.compile("^(speak|text|contents-richtext|toolTip|exData)$");
-
+    private static final String XFA_TEMPLATE_NS_PREFIX = 
"http://www.xfa.org/schema/xfa-template";;
     private static final String XFA_DATA_NS = 
"http://www.xfa.org/schema/xfa-data/1.0/";;
-
-    private static final String FIELD_LN = "field";
-    private static final QName XFA_DATA = new QName(XFA_DATA_NS, "data");
-
-    private final Matcher xfaTemplateMatcher;//namespace any version
-    private final Matcher textMatcher;
-
-    XFAExtractor() {
-        xfaTemplateMatcher = XFA_TEMPLATE_ANY_VERSION.matcher("");
-        textMatcher = TEXT_PATTERN.matcher("");
-    }
-
+    private static final Set<String> TEXT_ELEMENTS =
+            Set.of("speak", "text", "contents-richtext", "toolTip", "exData");
+    private static final Attributes EMPTY_ATTRIBUTES = new AttributesImpl();
+
+    /**
+     * @throws TikaException if the XFA stream itself is not well-formed XML; 
the
+     *                       caller may fall back to the AcroForm fields
+     * @throws SAXException  thrown by {@code xhtml}, propagated unchanged
+     */
     void extract(InputStream xfaIs, XHTMLContentHandler xhtml, Metadata m, 
ParseContext context)
-            throws XMLStreamException, SAXException {
+            throws IOException, SAXException, TikaException {
+        //the div must be closed even when the XFA is malformed, or the 
caller's
+        //AcroForm fallback nests under it and </body> can't balance
         xhtml.startElement("div", "class", "xfa_content");
+        try {
+            //tagging separates exceptions raised downstream from XML parse 
errors
+            TaggedContentHandler out = new TaggedContentHandler(xhtml);
+            XFAHandler handler = new XFAHandler(out);
+            try {
+                XMLReaderUtils.parseSAX(xfaIs, handler, context);
+            } catch (SAXException e) {
+                out.throwIfCauseOf(e);
+                throw new TikaException("XML error in XFA: " + e.getMessage(), 
e);
+            }
 
-        //TODO - replace this with multivalued map? This isn't
-        //actually metadata, just a handy data structure.
-        Metadata pdfObjRToValues = new Metadata();
-
-        //for now, store and dump the fields in insertion order
-        Map<String, XFAField> namedFields = new LinkedHashMap<>();
-
-        //The strategy is to cache the fields in fields
-        //and cache the values in pdfObjRToValues while
-        //handling the text etc along the way.
-        //
-        //As a final step, dump the merged fields and the values.
+            if (handler.fields.isEmpty()) {
+                return;
+            }
+            xhtml.startElement("div", "class", "xfa_form");
+            xhtml.startElement("ol");
+            StringBuilder sb = new StringBuilder();
+            for (Map.Entry<String, XFAField> e : handler.fields.entrySet()) {
+                String fieldName = e.getKey();
+                XFAField field = e.getValue();
+                String displayFieldName =
+                        (field.toolTip == null || field.toolTip.isBlank()) ? 
fieldName :
+                                field.toolTip;
+                List<String> fieldValues = 
handler.values.getOrDefault(fieldName,
+                        Collections.emptyList());
+                if (fieldValues.isEmpty()) {
+                    fieldValues = Collections.singletonList("");
+                }
+                for (String fieldValue : fieldValues) {
+                    AttributesImpl attrs = new AttributesImpl();
+                    attrs.addAttribute("", "fieldName", "fieldName", "CDATA", 
fieldName);
 
-        XMLStreamReader reader = 
XMLReaderUtils.getXMLInputFactory(context).createXMLStreamReader(xfaIs);
-        while (reader.hasNext()) {
-            switch (reader.next()) {
-                case XMLStreamConstants.START_ELEMENT:
-                    QName name = reader.getName();
-                    String localName = name.getLocalPart();
-                    if 
(xfaTemplateMatcher.reset(name.getNamespaceURI()).find() &&
-                            FIELD_LN.equals(name.getLocalPart())) {
-                        handleField(reader, namedFields);
-                    } else if (XFA_DATA.equals(name)) { //full qname match is 
important!
-                        loadData(reader, pdfObjRToValues);
-                    } else if (textMatcher.reset(localName).find()) {
-                        scrapeTextUntil(reader, xhtml, name);
+                    sb.append(displayFieldName).append(": ");
+                    if (fieldValue != null) {
+                        sb.append(fieldValue);
                     }
-                    break;
-                case XMLStreamConstants.END_ELEMENT:
-                    break;
-            }
-        }
 
-        if (namedFields.size() == 0) {
+                    xhtml.startElement("li", attrs);
+                    xhtml.characters(sb.toString());
+                    xhtml.endElement("li");
+                    sb.setLength(0);
+                }
+            }
+            xhtml.endElement("ol");
+            xhtml.endElement("div");
+        } finally {
             xhtml.endElement("div");
-            return;
         }
-        //now dump fields and values
-        xhtml.startElement("div", "class", "xfa_form");
-        xhtml.startElement("ol");
-        StringBuilder sb = new StringBuilder();
-        for (Map.Entry<String, XFAField> e : namedFields.entrySet()) {
-            String fieldName = e.getKey();
-            XFAField field = e.getValue();
-            String displayFieldName =
-                    (field.toolTip == null || field.toolTip.isBlank()) ? 
fieldName :
-                            field.toolTip;
-            String[] fieldValues = pdfObjRToValues.getValues(fieldName);
-            if (fieldValues.length == 0) {
-                fieldValues = new String[]{""};
-            }
-            for (String fieldValue : fieldValues) {
-                AttributesImpl attrs = new AttributesImpl();
-                attrs.addAttribute("", "fieldName", "fieldName", "CDATA", 
fieldName);
+    }
 
-                sb.append(displayFieldName).append(": ");
-                if (fieldValue != null) {
-                    sb.append(fieldValue);
-                }
+    private static boolean isTemplateField(String uri, String localName) {
+        return "field".equals(localName) && 
uri.startsWith(XFA_TEMPLATE_NS_PREFIX);
+    }
 
-                xhtml.startElement("li", attrs);
-                xhtml.characters(sb.toString());
-                xhtml.endElement("li");
-                sb.setLength(0);
-            }
-        }
-        xhtml.endElement("ol");
-        xhtml.endElement("div");
-        xhtml.endElement("div");
+    private static boolean isXfaData(String uri, String localName) {
+        return "data".equals(localName) && XFA_DATA_NS.equals(uri);
     }
 
-    //try to scrape the text until the endElement
-    private void scrapeTextUntil(XMLStreamReader reader, XHTMLContentHandler 
xhtml,
-                                 QName endElement) throws XMLStreamException, 
SAXException {
-        StringBuilder buffer = new StringBuilder();
-        boolean keepGoing = true;
-        while (reader.hasNext() && keepGoing) {
-            switch (reader.next()) {
-                case XMLStreamConstants.START_ELEMENT:
-                    break;
-                case XMLStreamConstants.CHARACTERS:
-                    int start = reader.getTextStart();
-                    int length = reader.getTextLength();
-                    buffer.append(reader.getTextCharacters(), start, length);
-                    break;
+    /**
+     * Streams paragraphs from text-bearing elements as they are seen and 
caches
+     * template fields and xfa:data values for the merged dump at the end.
+     */
+    private static class XFAHandler extends DefaultHandler {
 
-                case XMLStreamConstants.CDATA:
-                    start = reader.getTextStart();
-                    length = reader.getTextLength();
-                    buffer.append(reader.getTextCharacters(), start, length);
-                    break;
+        private enum State {
+            TOP, TEXT, DATA, FIELD, FIELD_TOOLTIP
+        }
+
+        //values keyed by the local name of the data element that carried them
+        final Map<String, List<String>> values = new LinkedHashMap<>();
+        //insertion order is dump order
+        final Map<String, XFAField> fields = new LinkedHashMap<>();
+
+        private final ContentHandler out;
+        private final StringBuilder buffer = new StringBuilder();
+        private State state = State.TOP;
+        //element whose end returns from TEXT or FIELD_TOOLTIP
+        private String endUri;
+        private String endLocalName;
+        private String fieldName;
+        private String toolTip;
+        private String pdfObjRef;
+
+        XFAHandler(ContentHandler out) {
+            this.out = out;
+        }
 
-                case (XMLStreamConstants.END_ELEMENT):
-                    if (reader.getName().equals(endElement)) {
-                        keepGoing = false;
-                    } else if ("p".equals(reader.getName().getLocalPart())) {
-                        xhtml.element("p", buffer.toString());
+        @Override
+        public void startElement(String uri, String localName, String qName, 
Attributes atts) {
+            switch (state) {
+                case TOP:
+                    if (isTemplateField(uri, localName)) {
+                        state = State.FIELD;
+                        fieldName = firstAttributeValue(atts, "name");
+                        toolTip = "";
+                        pdfObjRef = "";
+                    } else if (isXfaData(uri, localName)) {
+                        state = State.DATA;
                         buffer.setLength(0);
+                    } else if (TEXT_ELEMENTS.contains(localName)) {
+                        startScrape(State.TEXT, uri, localName);
                     }
                     break;
+                case FIELD:
+                    if ("toolTip".equals(localName)) {
+                        startScrape(State.FIELD_TOOLTIP, uri, localName);
+                    }
+                    break;
+                default:
+                    break;
             }
         }
-        String remainder = buffer.toString();
-        if (!remainder.isBlank()) {
-            xhtml.element("p", remainder);
-        }
-    }
 
+        private void startScrape(State scrapeState, String uri, String 
localName) {
+            state = scrapeState;
+            endUri = uri;
+            endLocalName = localName;
+            buffer.setLength(0);
+        }
 
-    private String scrapeTextUntil(XMLStreamReader reader, QName endElement)
-            throws XMLStreamException {
-        StringBuilder buffer = new StringBuilder();
-        boolean keepGoing = true;
-        while (reader.hasNext() && keepGoing) {
-            switch (reader.next()) {
-                case XMLStreamConstants.START_ELEMENT:
-                    break;
-                case XMLStreamConstants.CHARACTERS:
-                    int start = reader.getTextStart();
-                    int length = reader.getTextLength();
-                    buffer.append(reader.getTextCharacters(), start, length);
-                    break;
-
-                case XMLStreamConstants.CDATA:
-                    start = reader.getTextStart();
-                    length = reader.getTextLength();
-                    buffer.append(reader.getTextCharacters(), start, length);
-                    break;
+        private boolean isScrapeEnd(String uri, String localName) {
+            return endLocalName.equals(localName) && endUri.equals(uri);
+        }
 
-                case (XMLStreamConstants.END_ELEMENT):
-                    if (reader.getName().equals(endElement)) {
-                        keepGoing = false;
-                    } else if ("p".equals(reader.getName().getLocalPart())) {
-                        buffer.append("\n");
-                    }
-                    break;
+        @Override
+        public void characters(char[] ch, int start, int length) {
+            if (state == State.TEXT || state == State.DATA || state == 
State.FIELD_TOOLTIP) {
+                buffer.append(ch, start, length);
             }
         }
-        return buffer.toString();
-    }
 
-    private void loadData(XMLStreamReader reader, Metadata pdfObjRToValues)
-            throws XMLStreamException {
-        //reader is at the "xfa:data" element
-        //scrape the contents from the text containing nodes
-        StringBuilder buffer = new StringBuilder();
-        while (reader.hasNext()) {
-            switch (reader.next()) {
-                case (XMLStreamConstants.START_ELEMENT):
-                    break;
-                case XMLStreamConstants.CHARACTERS:
-                    int start = reader.getTextStart();
-                    int length = reader.getTextLength();
-                    buffer.append(reader.getTextCharacters(), start, length);
-                    break;
-
-                case XMLStreamConstants.CDATA:
-                    start = reader.getTextStart();
-                    length = reader.getTextLength();
-                    buffer.append(reader.getTextCharacters(), start, length);
+        @Override
+        public void endElement(String uri, String localName, String qName) 
throws SAXException {
+            switch (state) {
+                case TEXT:
+                    if (isScrapeEnd(uri, localName)) {
+                        if (!buffer.toString().isBlank()) {
+                            paragraph();
+                        }
+                        buffer.setLength(0);
+                        state = State.TOP;
+                    } else if ("p".equals(localName)) {
+                        paragraph();
+                        buffer.setLength(0);
+                    }
                     break;
-
-                case (XMLStreamConstants.END_ELEMENT):
+                case DATA:
+                    //text is attributed to whichever element ends next
                     if (buffer.length() > 0) {
-                        String localName = reader.getLocalName();
-                        pdfObjRToValues.add(localName, buffer.toString());
+                        values.computeIfAbsent(localName, k -> new 
ArrayList<>())
+                                .add(buffer.toString());
                         buffer.setLength(0);
                     }
-                    if (XFA_DATA.equals(reader.getName())) {
-                        return;
+                    if (isXfaData(uri, localName)) {
+                        state = State.TOP;
                     }
                     break;
-
-            }
-        }
-    }
-
-    private void handleField(XMLStreamReader reader, Map<String, XFAField> 
fields)
-            throws XMLStreamException {
-        //reader is set to the field element
-        String fieldName = findFirstAttributeValue(reader, "name");
-        String pdfObjRef = "";
-        String toolTip = "";
-        while (reader.hasNext()) {
-            switch (reader.next()) {
-                case XMLStreamConstants.START_ELEMENT:
-                    if ("toolTip".equals(reader.getName().getLocalPart())) {
-                        toolTip = scrapeTextUntil(reader, reader.getName());
+                case FIELD:
+                    if (isTemplateField(uri, localName)) {
+                        fields.put(fieldName, new XFAField(fieldName, toolTip, 
pdfObjRef));
+                        state = State.TOP;
                     }
-                    // add checkbutton, etcif (reader.getName().equals())
                     break;
-                case XMLStreamConstants.END_ELEMENT:
-                    if 
(xfaTemplateMatcher.reset(reader.getName().getNamespaceURI()).find() &&
-                            FIELD_LN.equals(reader.getName().getLocalPart())) {
-                        if (fieldName != null) {
-                            fields.put(fieldName, new XFAField(fieldName, 
toolTip, pdfObjRef));
-                        }
-                        return;
+                case FIELD_TOOLTIP:
+                    if (isScrapeEnd(uri, localName)) {
+                        toolTip = buffer.toString();
+                        buffer.setLength(0);
+                        state = State.FIELD;
+                    } else if ("p".equals(localName)) {
+                        buffer.append('\n');
                     }
                     break;
-                case XMLStreamConstants.PROCESSING_INSTRUCTION:
-                    if ("PDF_OBJR".equals(reader.getPITarget())) {
-                        pdfObjRef = reader.getPIData();
-                    }
+                default:
                     break;
+            }
+        }
 
+        @Override
+        public void processingInstruction(String target, String data) {
+            if (state == State.FIELD && "PDF_OBJR".equals(target)) {
+                pdfObjRef = data;
             }
         }
-    }
 
-    private String findFirstAttributeValue(XMLStreamReader reader, String 
name) {
-        for (int i = 0; i < reader.getAttributeCount(); i++) {
-            String n = reader.getAttributeLocalName(i);
-            if (name.equals(n)) {
-                return reader.getAttributeValue(i);
+        private void paragraph() throws SAXException {
+            if (buffer.length() == 0) {
+                return;
+            }
+            char[] chars = new char[buffer.length()];
+            buffer.getChars(0, chars.length, chars, 0);
+            out.startElement(XHTMLContentHandler.XHTML, "p", "p", 
EMPTY_ATTRIBUTES);
+            out.characters(chars, 0, chars.length);
+            out.endElement(XHTMLContentHandler.XHTML, "p", "p");
+        }
+
+        private static String firstAttributeValue(Attributes atts, String 
name) {
+            for (int i = 0; i < atts.getLength(); i++) {
+                if (name.equals(atts.getLocalName(i))) {
+                    return atts.getValue(i);
+                }
             }
+            return "";
         }
-        return "";
     }
 
     static class XFAField {
-        String fieldName;
-        String toolTip;
-        String pdfObjRef;
-        String value;
+        final String fieldName;
+        final String toolTip;
+        final String pdfObjRef;
 
-        public XFAField(String fieldName, String toolTip, String pdfObjRef) {
+        XFAField(String fieldName, String toolTip, String pdfObjRef) {
             this.fieldName = fieldName;
             this.toolTip = toolTip;
             this.pdfObjRef = pdfObjRef;
@@ -311,7 +292,7 @@ class XFAExtractor {
         @Override
         public String toString() {
             return "XFAField{" + "fieldName='" + fieldName + '\'' + ", 
toolTip='" + toolTip + '\'' +
-                    ", pdfObjRef='" + pdfObjRef + '\'' + ", value='" + value + 
'\'' + '}';
+                    ", pdfObjRef='" + pdfObjRef + '\'' + '}';
         }
     }
 }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/XFAExtractorTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/XFAExtractorTest.java
new file mode 100644
index 0000000000..b85d6abbc8
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/XFAExtractorTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.pdf;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream;
+import org.junit.jupiter.api.Test;
+import org.xml.sax.ContentHandler;
+
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.exception.WriteLimitReachedException;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.sax.BodyContentHandler;
+import org.apache.tika.sax.ToXMLContentHandler;
+import org.apache.tika.sax.XHTMLContentHandler;
+
+public class XFAExtractorTest {
+
+    private static final String XDP = "<xdp:xdp 
xmlns:xdp=\"http://ns.adobe.com/xdp/\";>\n" +
+            "<template 
xmlns=\"http://www.xfa.org/schema/xfa-template/3.3/\";>\n" +
+            " <subform name=\"form1\">\n" +
+            "  <field name=\"School_Name\">\n" +
+            "   <?PDF_OBJR 12 0 R?>\n" +
+            "   dropped: text directly under a field\n" +
+            "   
<assist><toolTip><p>School</p><p>Name</p></toolTip></assist>\n" +
+            "  </field>\n" +
+            "  <field name=\"Room_1\"><assist><toolTip>  
</toolTip></assist></field>\n" +
+            "  <field><assist><toolTip>Nameless</toolTip></assist></field>\n" +
+            "  <draw><value><text>Mount 
<![CDATA[Rushmore]]></text></value></draw>\n" +
+            "  
<draw><value><exData>intro<p>first</p><p></p>tail</exData></value></draw>\n" +
+            "  <draw><value><speak>  </speak></value></draw>\n" +
+            "  <assist><toolTip>Top-level tip</toolTip></assist>\n" +
+            " </subform>\n" +
+            "</template>\n" +
+            "<other><field name=\"Not_A_Template_Field\"/></other>\n" +
+            "<xfa:datasets 
xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\";>\n" +
+            " <xfa:data><form1>" +
+            "<School_Name>my_school</School_Name>" +
+            "<Room_1>my_room1</Room_1><Room_1>my_room2</Room_1>" +
+            "before<Unbound>inner</Unbound>after" +
+            "</form1></xfa:data>\n" +
+            "</xfa:datasets>\n" +
+            "</xdp:xdp>";
+
+    @Test
+    public void testFieldsValuesAndText() throws Exception {
+        String xml = extract(XDP, new ToXMLContentHandler());
+        String body = xml.substring(xml.indexOf("<body>") + "<body>".length(),
+                xml.indexOf("</body>"));
+        //text before a <p> merges into that paragraph; every </p> in a 
toolTip adds "\n"
+        assertEquals("<div class=\"xfa_content\"><p>Mount Rushmore</p>\n" +
+                "<p>introfirst</p>\n<p>tail</p>\n" +
+                "<p>Top-level tip</p>\n" +
+                "<div class=\"xfa_form\"><ol>" +
+                "\t<li fieldName=\"School_Name\">School\nName\n: 
my_school</li>\n" +
+                "\t<li fieldName=\"Room_1\">Room_1: my_room1</li>\n" +
+                "\t<li fieldName=\"Room_1\">Room_1: my_room2</li>\n" +
+                "\t<li fieldName=\"\">Nameless: </li>\n" +
+                "</ol>\n</div>\n" +
+                "</div>\n", body);
+    }
+
+    @Test
+    public void testMalformedIsTikaExceptionAndDivBalanced() throws Exception {
+        ToXMLContentHandler handler = new ToXMLContentHandler();
+        XHTMLContentHandler xhtml = xhtml(handler);
+        xhtml.startDocument();
+        try (InputStream is = stream("<xdp><template 
xmlns=\"http://www.xfa.org/schema/"; +
+                "xfa-template/3.3/\"><text>partial</text><field")) {
+            assertThrows(TikaException.class,
+                    () -> new XFAExtractor().extract(is, xhtml, new Metadata(),
+                            new ParseContext()));
+        }
+        xhtml.endDocument();
+        String xml = handler.toString();
+        assertTrue(xml.contains("<div 
class=\"xfa_content\"><p>partial</p>\n</div>\n</body>"), xml);
+    }
+
+    @Test
+    public void testDownstreamExceptionPropagatesUnwrapped() {
+        assertThrows(WriteLimitReachedException.class,
+                () -> extract(XDP, new BodyContentHandler(3)));
+    }
+
+    private static String extract(String xdp, ContentHandler handler) throws 
Exception {
+        XHTMLContentHandler xhtml = xhtml(handler);
+        xhtml.startDocument();
+        try (InputStream is = stream(xdp)) {
+            new XFAExtractor().extract(is, xhtml, new Metadata(), new 
ParseContext());
+        }
+        xhtml.endDocument();
+        return handler.toString();
+    }
+
+    private static XHTMLContentHandler xhtml(ContentHandler handler) {
+        return new XHTMLContentHandler(handler, new Metadata());
+    }
+
+    private static InputStream stream(String xml) throws Exception {
+        return UnsynchronizedByteArrayInputStream.builder()
+                .setByteArray(xml.getBytes(StandardCharsets.UTF_8)).get();
+    }
+}

Reply via email to