Debugging PDF Transformations - quality-manager/onboarding GitHub Wiki

Background

The PDF Export feature creates PDFs by fetching the XML representation of an artifact via the Reportable REST API then running that XML through a series of transformations. The final result of those transformations is an XSL-FO document which is fed to Apache FOP for conversion to PDF bytes.

For performance and scalability reasons, these transformations take place in a streamed fashion. The downside to this is that the transformation from beginning to end is kind of a black box. There are no intermidate states that can be inspected.

When debugging PDF issues, it is often useful to see the result of a partial transformation. This is especially true when trying to determine which of the many transformations is defective. This article describes how to accomplish this.

In the Dev Environment

Note, you should enable the PDF strict mode in Advanced Server properties. This will ensure that exceptions are thrown and handled by PDF so that you will get the stylesheet and line number information when an error occurs.

Method 1

If you are debugging in your Dev Environment (and therefore are able to change code) the easiest way to accomplish this is to modify the code found in the class AbstractPrinter as below.

View the state of the transformation after completing the "prep" transformation for a Summary PDF job and write that XML to a file located at /Users/johnsmith/Desktop/out.xml.

private byte[] printArtifactSummary() throws PrintRuntimeException {
    // ...

    Transformation.builder(transformationFactory)
      .setSource(getArtifactSummaryInput(target))
//    .setResult(newFopHandler(resultBytes))
      .setResult("/Users/johnsmith/Desktop/out.xml")
      .addTransform(newIdRemovalHandler())
      .addTransform(newVirtualAttachmentHandler())
      .addTransform(newTranslateToExecScriptHandler())
      .addTransform(newSummaryPrepHandler())
//    .addTransform(newTranslateToExecScriptHandler())
//    .addTransform(newExpandTablesHandler(renderingDetails))
//    .addTransform(newSummaryRenderHandler(renderingDetails))
//    .addTransform(newEncodeUrlsHandler())
//    .addTransform(newUpdateFontNamesHandler())
//    .addTransform(newBidiTextDirHandler())
      .build()
      .transform();

    // ...
}

Note that if you use this technique, your PDF jobs will show as failing as long as this code change is in place.

Method 2

The <xsl:message> can be used inline within a template to output to the console similar to System.out.println()

<xsl:message>
   <xsl:value-of select="rqm:executionworkitem/dc:identifier"/>
</xsl:message>

Method 3

We can also get information from the XSL call stack when debugging in Java.

  • FuncExtFunction.execute - contains the name of the java class and function being called from within a XSL template and the parameter values
    • this - click on this reference in debugger will reveal the Java class and method being invoked from the stylesheet
    • argVec - this variable will contain the list of values being sent to the Java method
  • Elem... - these classes (beginning with Elem) correspond to the XSL elements in the stylesheet. With an Eclipse detail formatter we can just click on them from the stack trace variables and see the stylesheet and linenumbers of the source.
    • this - click on this reference in debugger will show stylesheet and line number when using a detail formatter.

Creating Eclipse detail formatter for XSL Elements

  1. Open the eclipse detail formatter in Eclipse ( On preferences dialog just type in 'detail' in search box )
  2. Click Add...
  3. Set type as org.apache.xalan.templates.ElemTemplateElement
  4. Set snippet as this.getPublicId() + ", line: " + this.getLineNumber() + ", element: " + this.getLocalName()
  5. Click ok and then Apply and Close.

Debugging XSL-FO files

After getting the content of the XSL file from methods above we can debug its content by trying to convert it to PDF with next sample code:

/*
 * 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$ */

// Java
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.apps.Fop;
import org.apache.fop.apps.FopFactory;
import org.apache.fop.apps.FormattingResults;
import org.apache.fop.apps.MimeConstants;
import org.apache.fop.apps.PageSequenceResults;

public class PDFTest {

    // configure fopFactory as desired
    private final FopFactory fopFactory = FopFactory.newInstance(new File(".").toURI());

    /**
     * Converts an FO file to a PDF file using FOP
     * @param fo the FO file
     * @param pdf the target PDF file
     * @throws IOException In case of an I/O problem
     * @throws FOPException In case of a FOP problem
     */
    public void convertFO2PDF(File fo, File pdf) throws IOException, FOPException {

        OutputStream out = null;

        try {
            FOUserAgent foUserAgent = fopFactory.newFOUserAgent();
            // configure foUserAgent as desired

            // Setup output stream.  Note: Using BufferedOutputStream
            // for performance reasons (helpful with FileOutputStreams).
            out = new FileOutputStream(pdf);
            out = new BufferedOutputStream(out);

            // Construct fop with desired output format
            Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, out);

            // Setup JAXP using identity transformer
            TransformerFactory factory = TransformerFactory.newInstance();
            Transformer transformer = factory.newTransformer(); // identity transformer

            // Setup input stream
            Source src = new StreamSource(fo);

            // Resulting SAX events (the generated FO) must be piped through to FOP
            Result res = new SAXResult(fop.getDefaultHandler());

            // Start XSLT transformation and FOP processing
            transformer.transform(src, res);

            // Result processing
            FormattingResults foResults = fop.getResults();
            java.util.List pageSequences = foResults.getPageSequences();
            System.out.println("PageSequences: " + pageSequences.size());
            for (Object pageSequence : pageSequences) {
                PageSequenceResults pageSequenceResults = (PageSequenceResults) pageSequence;
                System.out.println("PageSequence "
                        + (String.valueOf(pageSequenceResults.getID()).length() > 0
                        ? pageSequenceResults.getID() : "<no id>")
                        + " generated " + pageSequenceResults.getPageCount() + " pages.");
            }
            System.out.println("Generated " + foResults.getPageCount() + " pages in total.");

        } catch (Exception e) {
            e.printStackTrace(System.err);
            System.exit(-1);
        } finally {
            out.close();
        }
    }


    /**
     * Main method.
     * @param args command-line arguments
     */
    public static void main(String[] args) {
        try {
            System.out.println("FOP ExampleFO2PDF\n");
            System.out.println("Preparing...");

            //Setup directories
            File baseDir = new File(".");
            File outDir = new File(baseDir, "out");
            outDir.mkdirs();

            //Setup input and output files
            File fofile = new File(baseDir, "out.xml");
            File pdffile = new File(outDir, "ResultFO2PDF.pdf");

            System.out.println("Input: XSL-FO (" + fofile + ")");
            System.out.println("Output: PDF (" + pdffile + ")");
            System.out.println();
            System.out.println("Transforming...");

            PDFTest app = new PDFTest();
            app.convertFO2PDF(fofile, pdffile);

            System.out.println("Success!");
        } catch (Exception e) {
            e.printStackTrace(System.err);
            System.exit(-1);
        }
    }
}

If any error appears we should see it in the exception. Find some examples next:

Transforming...
Sep 30, 2020 10:13:16 AM org.apache.fop.events.LoggingEventListener processEvent
SEVERE: Image not found. URI: basecache://com.ibm.asq.common.web/resources/ui/internal/images/icons/view16/qm_testscript48.gif. (See position 1:9645)
Sep 30, 2020 10:13:16 AM org.apache.fop.events.LoggingEventListener processEvent
SEVERE: Image not found. URI: basecache://com.ibm.rqm.planning.web/resources/ui/internal/view/templates/common/images/step.gif. (See position 3:6968)
FATAL ERROR:  'org.apache.fop.fo.ValidationException: A table-cell is spanning more rows than available in its parent element.'
           :A table-cell is spanning more rows than available in its parent element.
java.lang.NullPointerException
	at PDFTest.convertFO2PDF(PDFTest.java:91)
	at PDFTest.main(PDFTest.java:135)
Transforming...
Sep 30, 2020 10:33:43 AM org.apache.fop.events.LoggingEventListener processEvent
SEVERE: Image not found. URI: basecache://com.ibm.asq.common.web/resources/ui/internal/images/icons/view16/qm_testscript48.gif. (See position 1:9662)
FATAL ERROR:  'org.apache.fop.fo.ValidationException: Missing attribute on fo:basic-link: Either external-destination or internal-destination must be specified. (See position 18:885)'
           :file:out.xml:18:885: Missing attribute on fo:basic-link: Either external-destination or internal-destination must be specified. (See position 18:885)
java.lang.NullPointerException
	at PDFTest.convertFO2PDF(PDFTest.java:91)
	at PDFTest.main(PDFTest.java:135)
⚠️ **GitHub.com Fallback** ⚠️