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 <desc> section (govdocs1:
754282.pdf, 982106.pdf)</li>
* <li>handle pdf metadata (access permissions, etc.) in <pdf>
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();
+ }
+}