The software development community has shown a renewed interest in testing during the past few years. Much of this has been driven by the eXtreme Programming methodology, which emphasizes lightweight processes and constant unit testing to promote quality.[49] To demonstrate how to test XSLT transformations, a few simple files will be used. The XML data is shown first in Example 9-4.
[49] See http://www.xprogramming.com for more information on eXtreme Programming.
<?xml version="1.0" encoding="UTF-8"?> <person> <firstName>Aidan</firstName> <middleName>Garrett</middleName> <lastName>Burke</lastName> <birthDate month="6" day="25" year="1999"/> </person>
Although this data is trivial, the same concepts apply to larger, more realistic examples. The sample XSLT stylesheet is shown in Example 9-5.
<?xml version="1.0" encoding="UTF-8"?> <!-- *********************************************************** ** Transforms an XML file representing a person into a ** more concise format. ********************************************************--> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:param name="includeMiddle" select="'yes'"/> <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" doctype-system="condensed.dtd"/> <!-- match an existing <person> element --> <xsl:template match="person"> <!-- produce a new <person> element in a condensed form --> <xsl:element name="person"> <xsl:element name="name"> <xsl:value-of select="firstName"/> <xsl:text> </xsl:text> <xsl:if test="$includeMiddle = 'yes'"> <xsl:value-of select="middleName"/> <xsl:text> </xsl:text> </xsl:if> <xsl:value-of select="lastName"/> </xsl:element> <xsl:element name="birthDate"> <xsl:value-of select="birthDate/@month"/> <xsl:text>/</xsl:text> <xsl:value-of select="birthDate/@day"/> <xsl:text>/</xsl:text> <xsl:value-of select="birthDate/@year"/> </xsl:element> </xsl:element> </xsl:template> </xsl:stylesheet>
The job of this stylesheet is to transform XML data into a more concise format as shown in Example 9-6.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE person SYSTEM "condensed.dtd"> <person> <name>Aidan Garrett Burke</name> <birthDate>6/25/1999</birthDate> </person>
Finally, the DTD for the condensed XML file is shown in Example 9-7.
<!ELEMENT person (name, birthDate)> <!ELEMENT birthDate (#PCDATA)> <!ELEMENT name (#PCDATA)>
By providing a DTD for the expected XML output, a unit test can easily validate the result tree after performing one or more transformations. Such a test simply writes the transformation results to a file and then attempts to parse them using a validating XML parser.
JUnit is an open source testing framework available from http://www.junit.org. It is a lightweight tool designed to be used by programmers specifically for unit tests. Other tools are generally better for integration testing and functional testing, but these are not discussed here.
Since XSLT transformations can be performed independently of the remainder of an application, they are a perfect candidate for automated unit testing. A technology such as JSP, however, is quite difficult to test in an automated fashion because JSPs must be executed within the context of a JSP container and web browser.
An automated test is one that reports "success" or "failure" after execution and does not require a human being to interact as the test is running. For instance, requiring a user to type in specific values into HTML form fields and then look at the resulting web page is clearly not automated. Also, a test that merely displays a long text report is not automated because it requires a knowledgeable person to read over the report and inspect it for errors.
By automating tests, every developer can run an entire suite of tests by executing a command-line program. The test suite then reports exactly which tests failed and where, so the problems can be fixed immediately.
A key philosophy behind a successful unit testing methodology is the idea that every test must always run at 100 percent success. Now, when programmers change an XSLT stylesheet or XML code and a test suddenly fails, they know that their changes were likely the source of the problem. When "broken" tests are left in the project for long periods of time, developers will probably stop running the testing suite because it becomes too difficult to manually filter through all of the error reports caused by the broken tests.
WARNING: It is important to adopt a policy in which every developer runs the suite of unit tests before checking any modified code into a shared code repository such as CVS.
For XSLT transformations, performing a transformation and then validating against a DTD or Schema is the easiest kind of testing. Once the structure of the result is validated, additional tests can be performed to determine if the semantic content of the result is correct. For instance, a DTD can report that a <firstName> element is present, but it takes additional testing to determine if the content of <firstName> is actually the correct name.
Example 9-8 shows how to write a simple test fixture using the JUnit framework. JUnit describes fixtures as a group of unit tests.
package chap9; import java.io.*; import java.net.*; import java.util.*; // JAXP used for XSLT transformations import javax.xml.transform.*; import javax.xml.transform.stream.*; // JDOM used for XML parsing and validation import org.jdom.*; import org.jdom.input.*; // JUnit classes import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; import junit.textui.TestRunner; /** * An example JUnit test. This class performs an XSLT transformation * and validates the result. */ public class SampleUnitTest extends TestCase { private String workingDir; // input XML files private File aidanXMLFile; private File johnXMLFile; // a stylesheet that condenses the XML data private File condenseXSLTFile; // the transformation results private File aidanCondensedXMLFile; private File johnCondensedXMLFile; private TransformerFactory transFact; /** * All JUnit tests have a constructor that takes the test name. */ public SampleUnitTest(String name) { super(name); } /** * Initialization before each test[...] method is called. */ public void setUp( ) { // locate a file named test.properties in the chap9 package ResourceBundle rb = ResourceBundle.getBundle("chap9.test"); this.workingDir = rb.getString("chap9.workingDir"); assertNotNull(workingDir); assert("Unable to locate " + this.workingDir, new File(this.workingDir).exists( )); this.aidanXMLFile = new File(workingDir + File.separator + "aidan.xml"); this.johnXMLFile = new File(workingDir + File.separator + "john.xml"); this.condenseXSLTFile = new File(workingDir + File.separator + "condensePerson.xslt"); this.aidanCondensedXMLFile = new File(this.workingDir + File.separator + "aidanCondensed.xml"); this.johnCondensedXMLFile = new File(this.workingDir + File.separator + "johnCondensed.xml"); this.transFact = TransformerFactory.newInstance( ); } /** * Clean up after each test[...] method */ public void tearDown( ) { // the transformation results could be deleted here, but the // cleanup code is intentionally commented out so the // developer can see the generated files: // this.aidanCondensedXMLFile.delete( ); // this.johnCondensedXMLFile.delete( ); } /** * An individual unit test. */ public void testTransformWithTemplates( ) throws Exception { Templates templates = this.transFact.newTemplates( new StreamSource(this.condenseXSLTFile)); Transformer trans = templates.newTransformer( ); // do two transformations using the same Transformer trans.transform(new StreamSource(this.aidanXMLFile), new StreamResult(this.aidanCondensedXMLFile)); trans.transform(new StreamSource(this.johnXMLFile), new StreamResult(this.johnCondensedXMLFile)); // validate both files validateCondensedFile(this.aidanCondensedXMLFile, "Aidan Garrett Burke", "6/25/1999"); validateCondensedFile(this.johnCondensedXMLFile, "John Fitzgerald Kennedy", "5/29/1917"); } /** * Another unit test. */ public void testTransformer( ) throws Exception { Transformer trans = this.transFact.newTransformer( new StreamSource(this.condenseXSLTFile)); trans.transform(new StreamSource(this.aidanXMLFile), new StreamResult(this.aidanCondensedXMLFile)); validateCondensedFile(this.aidanCondensedXMLFile, "Aidan Garrett Burke", "6/25/1999"); } // a helper method used by each of the unit tests private void validateCondensedFile(File file, String expectedName, String expectedBirthDate) { try { // first do a simple validation against the DTD SAXBuilder builder = new SAXBuilder(true); // validate Document doc = builder.build(file); // now perform some additional checks Element nameElem = doc.getRootElement( ).getChild("name"); assertEquals("Name was not correct", expectedName, nameElem.getText( )); Element birthDateElem = doc.getRootElement( ).getChild("birthDate"); assertEquals("Birth date was not correct", expectedBirthDate, birthDateElem.getText( )); } catch (JDOMException jde) { fail("XML was not valid: " + jde.getMessage( )); } } /** * @return a TestSuite, which is a composite of Test objects. */ public static Test suite( ) { // uses reflection to locate each method named test[...] return new TestSuite(SampleUnitTest.class); } /** * Allow the unit tests to be invoked from the command line * in text-only mode. */ public static void main(String[] args) { TestRunner.run(suite( )); } }
First, notice that SampleUnitTest extends from junit.framework.TestCase. Each subclass of TestCase defines a fixture and can contain multiple individual unit tests. Each method that begins with the word "test" is a unit test. All of the private fields in SampleUnitTest are specific to our particular needs and are not part of the JUnit framework.
The constructor takes the name of a unit test as an argument:
public SampleUnitTest(String name) { super(name); }
The name argument is the test method name, and JUnit uses the Java reflection API to locate and instantiate the correct method. As we will see in a moment, this constructor is rarely called directly.
The setUp( ) method is called before each unit test is executed. As expected, this method is used to set up preconditions before a test is executed. Its counterpart is the tearDown( ) method, which is called just after each test is executed. If a fixture contains four unit test methods, then setUp( ) and tearDown( ) will each be called four times.
For our purposes, the setUp( ) method locates all of the files that will be used for XSLT transformations. These include XML input files, the XSLT stylesheet, and the XSLT result targets. It also performs some simple testing:
assertNotNull(workingDir); assert("Unable to locate " + this.workingDir, new File(this.workingDir).exists( ));
These assert( ) methods are part of the JUnit framework, causing test failures when the tested condition is not true.[50] These are the heart of what programmers write when creating unit tests and can be used in any of the test methods or in the setUp( ) and tearDown( ) methods. When an assertion is not true, JUnit reports an error message and the line number where the failure occurred. This is known as a test failure, which is different than a test error. An error is reported when JUnit catches an exception that one of the unit tests throws.
[50] JUnit 3.7 renamed the assert() method assertTrue() to avoid conflicts with the new JDK 1.4 assertion facility.
This first unit test in our example is the testTransformWithTemplates( ) method. Because this method name begins with "test," JUnit can use reflection to locate it. The job of this test is to merely perform an XSLT transformation using JAXP's Templates interface, delegating to the validateCondensedFile( ) method to do the actual testing. This approach is taken because the same testing code can be shared among a group of individual unit tests.
The validateCondensedFile( ) method performs two levels of testing. First, the result of the transformation is validated against its DTD. If an exception is thrown, the test fails:
fail("XML was not valid: " + jde.getMessage( ));
JUnit will intercept this failure and display the error message to the programmer running the test. If the validation succeeds, the unit test then uses the assertEquals( ) method to test some of the actual XML content:
assertEquals("Name was not correct", expectedName, nameElem.getText( ));
In this method, if the second two arguments are not equal, the provided error message is displayed and the test fails.
One key additional method is suite( ):
public static Test suite( ) { // uses reflection to locate each method named test[...] return new TestSuite(SampleUnitTest.class); }
This is useful because it automatically locates all methods whose names begin with "test" and adds them to a test suite. Both TestCase and TestSuite implement the Test interface; TestSuite is a composite of many individual Test objects. By organizing tests into suites, entire families of tests can be executed by running the suite. As expected with a composite pattern, test suites can also consist of other test suites. At some point, one top-level test suite can directly or indirectly include every other test in the application. Therefore, all tests can be executed with a single command.
To run the test from the command line, type the following command:
java chap9.SampleUnitTest
This works because the fixture contains the following main( ) method:
public static void main(String[] args) { TestRunner.run(suite( )); }
The TestRunner class is a command-line tool that reports the following output when all tests succeed:
Time: 1.081 OK (2 tests)
The two dots in the first line of output represent each of the test methods. As each unit test executes, a new dot appears. If a test fails, JUnit reports a stack trace, a (sometimes) descriptive message, and the line number of the failure. At the end, the number of tests, failures, and errors are reported.
JUnit also has a Swing GUI client that can be executed with the following command:
java junit.swingui.TestRunner chap9.SampleUnitTest
Figure 9-4 shows the graphical output when errors occur.
The rectangular area to the left of "U" is a progress bar that expands as tests are executed. When dozens or hundreds of tests are executed, the progress bar gives a good visual indication of how many tests have executed. It also changes from green to red when errors or failures occur, so programmers know exactly when something went wrong. The scrolling list in the middle of the screen shows individual test errors and failures, and the text area near the bottom of the screen shows details for the selected error.
The GUI interface is great for interactive testing, while the command-line interface is more appropriate for batch-mode, automated tests. These are the sorts of tests that execute as part of a nightly build process. We now move past unit testing software into the realm of custom application error handling using JAXP error listeners.
When performing XSLT transformations using JAXP, errors are typically reported to System.err. While this is sufficient for command-line transformations, some custom applications require more control over the error-reporting process. For this class of applications, the javax.xml.transform.ErrorListener interface is provided.
By implementing this interface, an application can capture and report detailed information about where transformation errors occur and why. In Example 9-9, a custom Swing table model is presented. This class implements the javax.xml.transform.ErrorListener interface and is used by a JTable to display errors graphically. Later, in Example 9-11, we show how to register this error listener with a TransformerFactory and Transformer.
package com.oreilly.javaxslt.swingtrans; import java.io.*; import java.util.*; import javax.swing.table.*; // XML-related imports import javax.xml.transform.ErrorListener; import javax.xml.transform.SourceLocator; import javax.xml.transform.TransformerException; /** * A JTable data model that provides detail information about a list * of javax.xml.transform.TransformerException objects. */ public class ErrorListenerModel extends AbstractTableModel implements ErrorListener { // column positions in the table private static final int LINE_COL = 0; private static final int COLUMN_COL = 1; private static final int PUBLIC_ID_COL = 2; private static final int SYSTEM_ID_COL = 3; private static final int MESSAGE_AND_LOC_COL = 4; private static final int LOCATION_COL = 5; private static final int EXCEPTION_COL = 6; private static final int CAUSE_COL = 7; private static final String[] COLUMN_NAMES = { "Line", "Column", "Public ID", "System ID", "Message & Location", "Location", "Exception", "Cause" }; // the actual data private List exceptionList = null; /** * @return a detailed text report of the exception at the specified row. */ public String getDetailReport(int row) { if (this.exceptionList == null || row < 0 || row >= this.exceptionList.size( )) { return ""; } TransformerException te = (TransformerException) this.exceptionList.get(row); SourceLocator loc = te.getLocator( ); // may be null // buffer the report StringWriter sw = new StringWriter( ); PrintWriter pw = new PrintWriter(sw); pw.println(te.getClass().getName( )); pw.println("-----------------------------------------------------"); if (loc == null) { pw.println("Line Number : [null SourceLocator]"); pw.println("Column Number: [null SourceLocator]"); pw.println("Public ID : [null SourceLocator]"); pw.println("System ID : [null SourceLocator]"); } else { pw.println("Line Number : " + loc.getLineNumber( )); pw.println("Column Number: " + loc.getColumnNumber( )); pw.println("Public ID : " + loc.getPublicId( )); pw.println("System ID : " + loc.getSystemId( )); } pw.println("Message & Location : " + te.getMessageAndLocation( )); pw.println("Location : " + te.getLocationAsString( )); pw.println("Exception : " + te.getException( )); if (te.getException( ) != null) { te.getException( ).printStackTrace(pw); } pw.println("Cause : " + te.getCause( )); if (te.getCause() != null && (te.getCause() != te.getException( ))) { te.getCause( ).printStackTrace(pw); } return sw.toString( ); } /** * Part of the TableModel interface. */ public Object getValueAt(int row, int column) { if (this.exceptionList == null) { return "No errors or warnings"; } else { TransformerException te = (TransformerException) this.exceptionList.get(row); SourceLocator loc = te.getLocator( ); switch (column) { case LINE_COL: return (loc != null) ? String.valueOf(loc.getLineNumber( )) : "N/A"; case COLUMN_COL: return (loc != null) ? String.valueOf(loc.getColumnNumber( )) : "N/A"; case PUBLIC_ID_COL: return (loc != null) ? loc.getPublicId( ) : "N/A"; case SYSTEM_ID_COL: return (loc != null) ? loc.getSystemId( ) : "N/A"; case MESSAGE_AND_LOC_COL: return te.getMessageAndLocation( ); case LOCATION_COL: return te.getLocationAsString( ); case EXCEPTION_COL: return te.getException( ); case CAUSE_COL: return te.getCause( ); default: return "[error]"; // shouldn't happen } } } /** * Part of the TableModel interface. */ public int getRowCount( ) { return (this.exceptionList == null) ? 1 : this.exceptionList.size( ); } /** * Part of the TableModel interface. */ public int getColumnCount( ) { return (this.exceptionList == null) ? 1 : COLUMN_NAMES.length; } /** * Part of the TableModel interface. */ public String getColumnName(int column) { return (this.exceptionList == null) ? "Transformation Problems" : COLUMN_NAMES[column]; } /** * @return true if any errors occurred. */ public boolean hasErrors( ) { return this.exceptionList != null; } /** * This is part of the javax.xml.transform.ErrorListener interface. * Indicates that a warning occurred. Transformers are required to * continue processing after warnings, unless the application * throws TransformerException. */ public void warning(TransformerException te) throws TransformerException { report(te); } /** * This is part of the javax.xml.transform.ErrorListener interface. * Indicates that a recoverable error occurred. */ public void error(TransformerException te) throws TransformerException { report(te); } /** * This is part of the javax.xml.transform.ErrorListener interface. * Indicates that a non-recoverable error occurred. */ public void fatalError(TransformerException te) throws TransformerException { report(te); } // adds the exception to exceptionList and notifies the JTable that // the content of the table has changed. private void report(TransformerException te) { if (this.exceptionList == null) { this.exceptionList = new ArrayList( ); this.exceptionList.add(te); fireTableStructureChanged( ); } else { this.exceptionList.add(te); int row = this.exceptionList.size( )-1; super.fireTableRowsInserted(row, row); } } }
Code related to the ErrorListener interface is emphasized; the remaining code is used to present errors in a Swing table. The Swing JTable component displays rows and columns of data, getting its information from an underlying javax.swing.table.TableModel interface. javax.swing.table.AbstractTableModel is an abstract class that implements TableModel, serving as the base class for application-defined table models as shown here. As you can see, ErrorListenerModel extends from AbstractTableModel.
Since our table model implements the ErrorListener interface, it can be attached to a JAXP Transformer. When transformation problems occur, warning( ), error( ), or fatalError( ) is called. Since these methods have the same signature, they all delegate to the report( ) method. Comments in the code indicate which types of problems are supposed to call each method, although XSLT processors are not consistent in the way they report errors.
The report( ) method simply adds the TransformerException object to a private list of exceptions and then triggers a Swing event to indicate that the JTable should redraw its contents. When the JTable receives this event, it asks the ErrorListenerModel for the row count, column count, and values at individual cells within the table model. This functionality is contained within the getRowCount( ), getColumnCount( ), and getValueAt( ) methods, all of which are defined in the TableModel interface.
Our class also has an additional method called getDetailReport( ), which is used to produce a text report of a TransformerException object. This method is worth studying because it shows which methods are available when dealing with transformation problems. As the code in Example 9-9 shows, many of the fields may be null. Some XSLT processors may provide a lot of detailed error reporting, while others may simply leave these fields null.
In this section, an XSLT transformer GUI is developed. This is a simple Swing application that allows an XML file to be transformed using an XSLT stylesheet. The results of the transformation are then shown in a text area, along with a JTable that shows all errors using the ErrorListenerModel class shown in Example 9-9.
XML validation of the transformation result is also provided. Given that the stylesheet produces XML, this tool will attempt to parse and validate the result tree. This is a great way to make sure that your XSLT is producing valid XHTML, for example, because the result can be validated against one of the XHTML DTDs as soon as each transformation is performed.
The Ant build file shown earlier in Example 9-3 contains a "run" target that can be used to execute this application by typing ant run.
The first window to appear is shown in Figure 9-5. This window is always displayed and allows the user to select the XML and XSLT input files.[51]
[51] These screen shots show the Macintosh OS/X Aqua look and feel using Java Swing.
When the Transform button is clicked, the window shown in Figure 9-6 appears. Subsequent transformations can be performed, and each one will cause an additional window to appear. Since the XML and XSLT files are parsed with each transformation, this application does not have to be restarted to see changes to those files.
The first tab, Text Output, is actually the first one to be displayed. It is not shown here because it merely shows the raw text contents of the XSLT result tree, which includes any whitespace produced by the transformation. When the user clicks on the XML Output tab, the result tree is parsed and validated against its DTD. The XML is then displayed in the GUI using JDOM's XMLOutputter class, which removes ignorable whitespace and pretty-prints the XML.
If errors occur during the transformation process, the text and XML output panels are blank. Instead, the user is presented with the display shown in Figure 9-7.
This display shows how the ErrorListenerModel presented in Example 9-9 is used. The JTable at the top of the screen shows a tabular view of all errors, and the text area at the bottom of the screen shows the output from ErrorListenerModel's getDetailReport( ) method. In the error shown, a select attribute was intentionally misspelled in an XSLT stylesheet as seelect.
The source code for the main window is shown in Example 9-10. This is a subclass of JFrame that allows the user to select XML and XSLT filenames. This class is almost entirely GUI-related code and is not discussed further.
package com.oreilly.javaxslt.swingtrans; import java.awt.*; import java.awt.event.*; import java.io.*; import javax.swing.*; /** * The entry point into this application. This class displays the main * window, allowing the user to select an XML file and an XSLT file. */ public class SwingTransformer extends JFrame { private JTextField xmlFileFld = new JTextField(30); private JTextField xsltFileFld = new JTextField(30); // file filters used with the JFileChooser class private XMLFileFilter xmlFilter = new XMLFileFilter( ); private XSLTFileFilter xsltFilter = new XSLTFileFilter( ); private JFileChooser fileChooser = new JFileChooser( ); // actions are hooked up to the JButtons private Action loadXMLAction = new javax.swing.AbstractAction("Select XML") { public void actionPerformed(ActionEvent evt) { selectXMLFile( ); } }; private Action loadXSLTAction = new javax.swing.AbstractAction("Select XSLT") { public void actionPerformed(ActionEvent evt) { selectXSLTFile( ); } }; private Action transformAction = new javax.swing.AbstractAction("Transform") { public void actionPerformed(ActionEvent evt) { File xmlFile = new File(xmlFileFld.getText( )); File xsltFile = new File(xsltFileFld.getText( )); if (!xmlFile.exists() || !xmlFile.canRead( )) { showErrorDialog("Unable to read XML file"); return; } if (!xsltFile.exists() || !xsltFile.canRead( )) { showErrorDialog("Unable to read XSLT file"); return; } // show the results of the transformation in a new window new TransformerWindow( ).transform(xmlFile, xsltFile); } }; /** * The entry point into the application; shows the main window. */ public static void main(String[] args) { new SwingTransformer( ).setVisible(true); } /** * Construct the main window and layout the GUI. */ public SwingTransformer( ) { super("Swing XSLT Transformer"); // note: this line requires Java 2 v1.3 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Container cp = getContentPane( ); cp.setLayout(new GridBagLayout( )); GridBagConstraints gbc = new GridBagConstraints( ); gbc.anchor = GridBagConstraints.WEST; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.gridx = GridBagConstraints.RELATIVE; gbc.gridy = 0; gbc.insets.top = 2; gbc.insets.left = 2; gbc.insets.right = 2; cp.add(new JLabel("XML File:"), gbc); gbc.weightx = 1.0; cp.add(this.xmlFileFld, gbc); gbc.weightx = 0.0; cp.add(new JButton(this.loadXMLAction), gbc); gbc.gridy++; cp.add(new JLabel("XSLT File:"), gbc); gbc.weightx = 1.0; cp.add(this.xsltFileFld, gbc); gbc.weightx = 0.0; cp.add(new JButton(this.loadXSLTAction), gbc); gbc.gridy++; gbc.gridx = 0; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.anchor = GridBagConstraints.CENTER; gbc.fill = GridBagConstraints.NONE; cp.add(new JButton(this.transformAction), gbc); pack( ); } /** * Show the file chooser, listing all XML files. */ private void selectXMLFile( ) { this.fileChooser.setDialogTitle("Select XML File"); this.fileChooser.setFileFilter(this.xmlFilter); int retVal = this.fileChooser.showOpenDialog(this); if (retVal == JFileChooser.APPROVE_OPTION) { this.xmlFileFld.setText( this.fileChooser.getSelectedFile().getAbsolutePath( )); } } /** * Show the file chooser, listing all XSLT files. */ private void selectXSLTFile( ) { this.fileChooser.setDialogTitle("Select XSLT File"); this.fileChooser.setFileFilter(this.xsltFilter); int retVal = this.fileChooser.showOpenDialog(this); if (retVal == JFileChooser.APPROVE_OPTION) { this.xsltFileFld.setText( this.fileChooser.getSelectedFile().getAbsolutePath( )); } } private void showErrorDialog(String msg) { JOptionPane.showMessageDialog(this, msg, "Error", JOptionPane.ERROR_MESSAGE); } } /** * Used with JFileChooser to only show files ending with .xml or .XML. */ class XMLFileFilter extends javax.swing.filechooser.FileFilter { public boolean accept(File f) { String name = f.getName( ); return f.isDirectory( ) || name.endsWith(".xml") || name.endsWith(".XML"); } public String getDescription( ) { return "XML Files"; } } /** * Used with JFileChooser to only show files ending with .xslt or .XSLT. */ class XSLTFileFilter extends javax.swing.filechooser.FileFilter { public boolean accept(File f) { String name = f.getName( ); return f.isDirectory( ) || name.endsWith(".xsl") || name.endsWith(".xslt") || name.endsWith(".XSL") || name.endsWith(".XSLT"); } public String getDescription( ) { return "XSLT Files"; } }
The next class, shown in Example 9-11, creates the window shown in Figures 9-5 and 9-6. Much of this code is responsible for arranging the JTabbedPane component, which contains the three folder tabs that make up the window. This class also does the actual XSLT transformation; these lines are emphasized.
package com.oreilly.javaxslt.swingtrans; import java.awt.*; import java.awt.event.*; import java.io.*; import javax.swing.*; import javax.swing.table.*; import javax.swing.event.*; // XML-related imports import javax.xml.transform.SourceLocator; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; /** * A secondary JFrame that shows the result of a single XSLT * transformation. This frame has a JTabbedPane interface, showing * the transformation result, error messages, and the XML output. */ public class TransformerWindow extends JFrame { // the result of the XSLT transformation as text private String resultText; private JTabbedPane tabPane = new JTabbedPane( ); private JTextArea textOutputArea = new JTextArea(30, 70); private XMLOutputPanel xmlOutputPanel = new XMLOutputPanel( ); private ErrorListenerModel errModel = new ErrorListenerModel( ); private JTable errorTable = new JTable(this.errModel); private JTextArea errorDetailArea = new JTextArea(10, 70); private String xsltURL; /** * Construct a new instance and layout the GUI components. */ public TransformerWindow( ) { super("XSLT Transformation"); // add the tab pane to the frame Container cp = getContentPane( ); cp.add(this.tabPane, BorderLayout.CENTER); // add individual tabs this.tabPane.add("Text Output", new JScrollPane(this.textOutputArea)); this.tabPane.add("Transformation Problems", createErrorPanel( )); this.tabPane.add("XML Output", this.xmlOutputPanel); // listen to new tab selections this.tabPane.addChangeListener(new ChangeListener( ) { public void stateChanged(ChangeEvent evt) { tabChanged( ); } }); this.textOutputArea.setEditable(false); // listen to selection changes on the table of errors this.errorTable.getSelectionModel( ).addListSelectionListener( new ListSelectionListener( ) { public void valueChanged(ListSelectionEvent evt) { if (!evt.getValueIsAdjusting( )) { showErrorDetails( ); } } }); pack( ); } /** * Show details for the currently selected error. */ private void showErrorDetails( ) { int selRow = this.errorTable.getSelectedRow( ); this.errorDetailArea.setText(this.errModel.getDetailReport(selRow)); } /** * Perform an XSLT transformation. */ public void transform(File xmlFile, File xsltFile) { setVisible(true); try { // figure out the directory of the XSLT file. This will be // used to locate the DTD if (xsltFile != null) { File xsltDir = xsltFile.getParentFile( ); if (xsltDir.isDirectory( )) { this.xsltURL = xsltDir.toURL().toExternalForm( ); } } TransformerFactory transFact = TransformerFactory.newInstance( ); // register the table model as an error listener transFact.setErrorListener(this.errModel); Transformer trans = transFact.newTransformer( new StreamSource(xsltFile)); // check for null, because the factory might not throw // exceptions when the call to newTransformer( ) fails. This // is because we registered an error listener that does not // throw exceptions. if (trans != null) { trans.setErrorListener(this.errModel); // capture the result of the XSLT transformation StringWriter sw = new StringWriter( ); trans.transform(new StreamSource(xmlFile), new StreamResult(sw)); // show the results this.resultText = sw.toString( ); this.textOutputArea.setText(this.resultText); } } catch (TransformerConfigurationException tce) { try { this.errModel.fatalError(tce); } catch (TransformerException ignored) { } } catch (TransformerException te) { try { this.errModel.fatalError(te); } catch (TransformerException ignored) { } } catch (Exception unexpected) { System.err.println( "The XSLT processor threw an unexpected exception"); unexpected.printStackTrace( ); } // show the error tab if (this.errModel.hasErrors( )) { this.tabPane.setSelectedIndex(1); } } // the user clicked on a different tab private void tabChanged( ) { try { setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); int selIndex = this.tabPane.getSelectedIndex( ); String selTab = this.tabPane.getTitleAt(selIndex); // when the XML tab is selected, set the text on the XML panel. // Although the text may not be XML, we won't know that until // it is parsed. if ("XML Output".equals(selTab)) { this.xmlOutputPanel.setXML(this.resultText, this.xsltURL); } } finally { setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } } // a helper method to create the panel that displays errors private JComponent createErrorPanel( ) { JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); this.errorTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); int size = this.errorDetailArea.getFont().getSize( ); this.errorDetailArea.setEditable(false); this.errorDetailArea.setFont( new Font("Monospaced", Font.PLAIN, size+2)); splitPane.setTopComponent(new JScrollPane(this.errorTable)); splitPane.setBottomComponent(new JScrollPane(this.errorDetailArea)); return splitPane; } }
As the emphasized code shows, the error listener table model is registered on the TransformerFactory as well as the Transformer instance. In addition to registering the error listener, exceptions also have to be caught because XSLT processors may still throw exceptions and errors even though an error listener is registered. Generally, errors on the TransformerFactory indicate problems while parsing the XSLT stylesheet, while Transformer error listeners are notified of problems with the actual transformation or in the XML data.
The final class, XMLOutputPanel, is shown in Example 9-12.
package com.oreilly.javaxslt.swingtrans; import java.awt.*; //import java.awt.event.*; import java.io.*; import javax.swing.*; // XML-related imports import org.jdom.Document; import org.jdom.input.SAXBuilder; import org.jdom.output.XMLOutputter; /** * Displays XML text in a scrolling text area. A status label indicates * whether or not the XML is well formed and valid. */ public class XMLOutputPanel extends JPanel { // displays the XML private JTextArea xmlArea = new JTextArea(20,70); private String xml; private JLabel statusLabel = new JLabel( ); /** * Construct the panel and layout the GUI components. */ public XMLOutputPanel( ) { super(new BorderLayout( )); add(new JScrollPane(this.xmlArea), BorderLayout.CENTER); add(this.statusLabel, BorderLayout.NORTH); } /** * @param xml the actual XML data to display. * @param uri the location of the XML, thus allowing the parser * to locate the DTD. */ public void setXML(String xml, String uri) { // return quickly if the XML has already been set if (xml == null || xml.equals(this.xml)) { return; } this.xml = xml; // use JDOM to parse the XML Document xmlDoc = null; try { // attempt to validate the XML SAXBuilder saxBuilder = new SAXBuilder(true); xmlDoc = saxBuilder.build(new StringReader(this.xml), uri); this.statusLabel.setText("XML is well formed and valid"); } catch (Exception ignored) { // the data is not valid, but we should parse it again // to see if it is well formed } if (xmlDoc == null) { try { // don't validate SAXBuilder saxBuilder = new SAXBuilder(false); xmlDoc = saxBuilder.build(new StringReader(this.xml)); this.statusLabel.setText("XML is well formed, but not valid"); } catch (Exception ex) { this.statusLabel.setText("Data is not well formed XML"); // show the stack trace in the text area StringWriter sw = new StringWriter( ); ex.printStackTrace(new PrintWriter(sw)); this.xmlArea.setText(sw.toString( )); } } // if the document was parsed, show it if (xmlDoc != null) { try { // pretty-print the XML by indenting two spaces XMLOutputter xmlOut = new XMLOutputter(" ", true); StringWriter sw = new StringWriter( ); xmlOut.output(xmlDoc, sw); this.xmlArea.setText(sw.toString( )); } catch (Exception ex) { this.statusLabel.setText("Data could not be displayed."); // show the stack trace in the text area StringWriter sw = new StringWriter( ); ex.printStackTrace(new PrintWriter(sw)); this.xmlArea.setText(sw.toString( )); } } } }
XMLOutputPanel is responsible for parsing the result tree to determine if it is well-formed and valid XML. It starts by parsing the text using a validating parser, simply ignoring errors. If no errors occur, the document is well-formed, valid XML and can be displayed in the text area. Otherwise, the document is parsed again, only without any validation. This allows the code to determine if the XML is at least well-formed.
If the document is not well-formed or valid, the parser's stack trace is displayed in the GUI. For many XSLT transformations, the result tree may not be XML, and this message can simply be ignored. When errors do occur, however, this should make it much easier to locate the problems.
Copyright © 2002 O'Reilly & Associates. All rights reserved.