3.36. XFA Daten

Überblick

Die XML Forms Architecture, (XFA) ist eine Erweiterung der PDF-Strukturen um XML-Informationen, mit dem Ziel, PDF-Formulare in den Prozessen eines Workflow's besser verarbeiten zu können.

XFA Formulare sind nicht kompatibel zu AcroForms. Deshalb sind die PDFUnit-Tests für Acroforms auch nicht verwendbar. Test auf XFA-Daten basieren überwiegend auf XPath:

// Methods around XFA data:
.hasXFAData()
.hasXFAData().matchingXPath(..) 
.hasXFAData().withNode(..)

.hasNoXFAData()

Existenz und Abwesenheit von XFA

Der erste Test zielt auf die reine Existenz von XFA-Daten:

@Test
public void hasXFAData() throws Exception {
  String filename = "documentUnderTest.pdf";

  AssertThat.document(filename)
            .hasXFAData()
  ;
}

Es ist auch möglich, explizit zu testen, dass ein PDF-Dokument keine XFA-Daten enthält:

@Test
public void hasNoXFAData() throws Exception {
  String filename = "documentUnderTest.pdf";
  
  AssertThat.document(filename)
            .hasNoXFAData()
  ;
}

Einzelne XML-Tags validieren

Das im nächsten Beispiel verwendete PDF-Dokument enthält folgende XFA-Daten (Ausschnitt):

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
  ...
  <x:xmpmeta xmlns:x="adobe:ns:meta/"
             x:xmptk="Adobe XMP Core 4.2.1-c041 52.337767, 2008/04/13-15:41:00"
  >
    <config xmlns="http://www.xfa.org/schema/xci/2.6/">
      ...
      <log xmlns="http://www.xfa.org/schema/xci/2.6/">
        <to>memory</to>
        <mode>overwrite</mode>
      </log>
      ...
    </config>
    <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
      ...
      <rdf:Description xmlns:xmp="http://ns.adobe.com/xap/1.0/" >
        <xmp:MetadataDate>2009-12-03T17:50:52Z</xmp:MetadataDate>
      </rdf:Description>
      ...
    </rdf:RDF>
  </x:xmpmeta>
 ...
</xdp:xdp>

Um auf einen bestimmten Knoten zu testen, muss eine Instanz von com.pdfunit.XMLNode mit dem XPath-Ausdruck für den Knoten erzeugt werden. Als zweiter Parameter wird der erwartete Wert übergeben:

@Test
public void hasXFAData_WithNode() throws Exception {
  String filename = "documentUnderTest.pdf";
  XMLNode xmpNode = new XMLNode("xmp:MetadataDate", "2009-12-03T17:50:52Z");  1
  
  AssertThat.document(filename)
            .hasXFAData()
            .withNode(xmpNode)
  ;
}

1

PDFUnit analysiert die XFA-Daten des aktuellen PDF-Dokumentes und ermittelt die Namensräume selbständig, lediglich der Default-Namespace muss angegeben werden.

Für die internen Verarbeitung ergänzt PDFUnit vor dem Knoten den Pfad-Bestandteil "//". Aus diesem Grund darf der Knoten im Test kein Pfad sein, der die Wurzel "/" enthält.

Sollte der XPath-Ausdruck für den Knoten zu mehreren Treffern führen, wird der erste Treffer verwendet.

Wenn das erwartete Ergebnis für den XPath-Ausdruck bei der Erzeugung der XMLNode-Instanz angegeben wird, dann wird dieses auch mit dem tatsächlichen Ergebnis verglichen. Andernfalls wird nur die Existenz des Knotens festgestellt.

Prüfungen auf Attribut-Knoten sind selbstverständlich auch möglich:

@Test
public void hasXFAData_WithNode_NamespaceDD() throws Exception {
  String filename = "documentUnderTest.pdf";
  XMLNode ddNode = new XMLNode("dd:dataDescription/@dd:name", "movie");
  
  AssertThat.document(filename)
            .hasXFAData()
            .withNode(ddNode)
  ;
}

XPath-basierte XFA-Tests

In PDFUnit gibt es noch die Methode matchingXPath(..), um das ganze Potential von XPath nutzen zu können. Die nächsten beiden Beispiele zeigen, was damit machbar ist:

@Test
public void hasXFAData_MatchingXPath_FunctionStartsWith() throws Exception {
  String filename = "documentUnderTest.pdf";
  String xpathString = "starts-with(//dd:dataDescription/@dd:name, 'mov')";
  XPathExpression expressionWithFunction = new XPathExpression(xpathString);
  
  AssertThat.document(filename)
            .hasXFAData()
            .matchingXPath(expressionWithFunction) 
  ;
}
@Test
public void hasXFAData_MatchingXPath_FunctionCount_MultipleInvocation() throws Exception {
  String filename = "documentUnderTest.pdf";
  
  String xpathProducer = "//pdf:Producer[. ='Adobe LiveCycle Designer ES 8.2']";
  String xpathPI       = "count(//processing-instruction()) = 30";

  XPathExpression exprPI       = new XPathExpression(xpathPI);
  XPathExpression exprProducer = new XPathExpression(xpathProducer);
  
  AssertThat.document(filename)
            .hasXFAData()
            .matchingXPath(exprProducer)
            .matchingXPath(exprPI)
  ;
  
  // The same test in a different style:
  AssertThat.document(filename)
            .hasXFAData().matchingXPath(exprProducer)
            .hasXFAData().matchingXPath(exprPI)
  ;
}

Eine kleine Einschränkung muss genannt werden. Die XPath-Ausdrücke können nur mit den Möglichkeiten ausgewertet werden, die die verwendete XPath-Implementierung bietet. PDFUnit nutzt normalerweise die JAXP-Implementierung des verwendeten JDK. Damit ist die XPath-Kompatibilität aber vom JDK/JRE abhängig und unterliegt dem Wandel der Zeit.

Das Kapitel 13.12: „JAXP-Konfiguration“ erläutert am Beispiel von Xerces, wie ein beliebiger XML-Parser genutzt werden kann.

Default-Namensraum in XPath

XML-Namensräume werden automatisch ermittelt. Wenn aber ein Default-Namensraum verwendet wird, muss dieser als Instanz von DefaultNamespace angegeben werden. Für diese Instanz muss in Java ein Prefix verwendet werden. Der Wert für das Prefix kann beliebig sein:

@Test
public void hasXFAData_WithDefaultNamespace_XPathExpression() throws Exception {
  String filename = "documentUnderTest.pdf";
  
  String namespaceURI  = "http://www.xfa.org/schema/xfa-template/2.6/";
  String xpathSubform  = "count(//default:subform[@name ='movie']//default:field) = 5";

  DefaultNamespace defaultNS   = new DefaultNamespace(namespaceURI);
  XPathExpression exprSubform  = new XPathExpression(xpathSubform, defaultNS);
  
  AssertThat.document(filename)
            .hasXFAData()
            .matchingXPath(exprSubform)
  ;
}

Auch bei der Verwendung der Klasse XMLNode muss der Default-Namensraum mitgegeben werden:

/**
 * The default namespace has to be declared, 
 * but any alias can be used for it.
 */
@Test
public void hasXFAData_WithDefaultNamespace_XMLNode() throws Exception {
  String filename = "documentUnderTest.pdf";
  
  String namespaceXCI = "http://www.xfa.org/schema/xci/2.6/";
  DefaultNamespace defaultNS = new DefaultNamespace(namespaceXCI);
  XMLNode aliasFoo = new XMLNode("foo:log/foo:to", "memory", defaultNS);

  AssertThat.document(filename)
            .hasXFAData()
            .withNode(aliasFoo)
  ;
}