source: main/trunk/greenstone3/src/java/org/greenstone/gsdl3/util/XMLTransformer.java@ 25445

Last change on this file since 25445 was 25445, checked in by ak19, 12 years ago

Error reporting is now improved again, much better than in previous commit: no longer just writes out the XSLT stylesheet to the log file, but follows Dr Bainbridge and Sam's suggestion of performing the transformation that failed again with physical files instead of in-memory as before. This points out the exact line location of errors.

  • Property svn:keywords set to Author Date Id Revision
File size: 21.1 KB
Line 
1/*
2 * XMLTransformer.java
3 * Copyright (C) 2002 New Zealand Digital Library, http://www.nzdl.org
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 */
19package org.greenstone.gsdl3.util;
20
21import org.greenstone.util.GlobalProperties;
22
23// XML classes
24import javax.xml.transform.Transformer;
25import javax.xml.transform.TransformerFactory;
26import javax.xml.transform.TransformerConfigurationException;
27import javax.xml.transform.TransformerException;
28import javax.xml.transform.ErrorListener;
29
30import javax.xml.transform.stream.StreamSource;
31import javax.xml.transform.dom.DOMSource;
32import javax.xml.transform.Source;
33import javax.xml.transform.stream.StreamResult;
34import javax.xml.transform.dom.DOMResult;
35
36import javax.xml.parsers.DocumentBuilderFactory;
37import javax.xml.parsers.DocumentBuilder;
38import org.w3c.dom.Element;
39import org.w3c.dom.Document;
40
41import org.w3c.dom.Node;
42import org.w3c.dom.NodeList;
43
44// other java classes
45import java.io.StringReader;
46import java.io.StringWriter;
47import java.io.BufferedReader;
48import java.io.FileReader;
49import java.io.FileWriter;
50import java.io.File;
51import java.util.HashMap;
52import java.util.Set;
53import java.util.Map;
54import java.util.Iterator;
55
56import org.apache.xml.utils.DefaultErrorHandler;
57
58import org.apache.log4j.*;
59
60/** XMLTransformer - utility class for greenstone
61 *
62 * transforms xml using xslt
63 *
64 * @author <a href="mailto:[email protected]">Katherine Don</a>
65 * @version $Revision: 25445 $
66 */
67public class XMLTransformer {
68 private static int debugFileCount = 0; // for unique filenames when debugging XML transformations with physical files
69
70 static Logger logger = Logger.getLogger(org.greenstone.gsdl3.util.XMLTransformer.class.getName());
71
72 /** The transformer factory we're using */
73 TransformerFactory t_factory=null;
74
75 /**
76 * The no-arguments constructor.
77 *
78 * Any exceptions thrown are caught internally
79 *
80 * @see javax.xml.transform.TransformerFactory
81 */
82 public XMLTransformer() {
83 // http://download.oracle.com/docs/cd/E17476_01/javase/1.5.0/docs/api/index.html?javax/xml/transform/TransformerFactory.html states that
84 // TransformerFactory.newInstance() looks in jar files for a Factory specified in META-INF/services/javax.xml.transform.TransformerFactory,
85 // else it will use the "platform default"
86 // In this case: xalan.jar's META-INF/services/javax.xml.transform.TransformerFactory contains org.apache.xalan.processor.TransformerFactoryImpl
87 // as required.
88
89 // This means we no longer have to do a System.setProperty("javax.xml.transform.TransformerFactory", "org.apache.xalan.processor.TransformerFactoryImpl");
90 // followed by a this.t_factory = org.apache.xalan.processor.TransformerFactoryImpl.newInstance();
91 // The System.setProperty step to force the TransformerFactory implementation that gets used, conflicts with
92 // Fedora (visiting the Greenstone server pages breaks the Greenstone-tomcat hosted Fedora pages) as Fedora
93 // does not include the xalan.jar and therefore can't then find the xalan TransformerFactory explicitly set.
94
95 // Gone back to forcing use of xalan transformer, since other jars like crimson.jar, which may be on some
96 // classpaths, could be be chosen as the TransformerFactory implementation over xalan. This is what used to
97 // give problems before. Instead, have placed copies of the jars that Fedora needs (xalan.jar and serializer.jar
98 // and the related xsltc.jar which it may need) into packages/tomcat/lib so that it's on the server's classpath
99 // and will be found by Fedora.
100
101 // make sure we are using the xalan transformer
102 System.setProperty("javax.xml.transform.TransformerFactory", "org.apache.xalan.processor.TransformerFactoryImpl");
103 try {
104 this.t_factory = org.apache.xalan.processor.TransformerFactoryImpl.newInstance();
105 //this.t_factory = TransformerFactory.newInstance();
106 } catch (Exception e) {
107 logger.error("exception creating t_factory "+e.getMessage());
108 }
109 }
110
111
112
113 /**
114 * Transform an XML document using a XSLT stylesheet
115 *
116 * @param stylesheet a filename for an XSLT stylesheet
117 * @param xml_in the XML to be transformed
118 * @return the transformed XML
119 */
120 public String transform(String stylesheet, String xml_in) {
121
122 try {
123 // Use the TransformerFactory to process the stylesheet Source and generate a Transformer.
124 Transformer transformer = this.t_factory.newTransformer(new StreamSource(stylesheet));
125
126 // Use the Transformer to transform an XML Source and send the output to a Result object.
127 StringWriter output = new StringWriter();
128 StreamSource streamSource = new StreamSource(new StringReader(xml_in));
129 transformer.setErrorListener(new TransformErrorListener(stylesheet, streamSource));
130 transformer.transform(streamSource, new StreamResult(output));
131 return output.toString();
132 } catch (TransformerConfigurationException e) {
133 logger.error("couldn't create transformer object: "+e.getMessageAndLocation());
134 logger.error(e.getLocationAsString());
135 return "";
136 } catch (TransformerException e) {
137 logger.error("couldn't transform the source: " + e.getMessageAndLocation());
138 return "";
139 }
140 }
141
142 public String transformToString(Document stylesheet, Document source) {
143 return transformToString(stylesheet, source, null);
144 }
145
146
147 public String transformToString(Document stylesheet, Document source, HashMap parameters) {
148
149 try {
150 // Use the TransformerFactory to process the stylesheet Source and generate a Transformer.
151 Transformer transformer = this.t_factory.newTransformer(new DOMSource(stylesheet));
152 if (parameters != null) {
153 Set params = parameters.entrySet();
154 Iterator i = params.iterator();
155 while (i.hasNext()) {
156 Map.Entry m = (Map.Entry)i.next();
157 transformer.setParameter((String)m.getKey(), m.getValue());
158 }
159 }
160 //transformer.setParameter("page_lang", source.getDocumentElement().getAttribute(GSXML.LANG_ATT));
161
162
163 // Use the Transformer to transform an XML Source and send the output to a Result object.
164 StringWriter output = new StringWriter();
165 DOMSource domSource = new DOMSource(source);
166
167 transformer.setErrorListener(new TransformErrorListener(stylesheet, domSource));
168 transformer.transform(domSource, new StreamResult(output));
169 return output.toString();
170 } catch (TransformerConfigurationException e) {
171 logger.error("couldn't create transformer object: "+e.getMessageAndLocation());
172 logger.error(e.getLocationAsString());
173 return "";
174 } catch (TransformerException e) {
175 logger.error("couldn't transform the source: " + e.getMessageAndLocation());
176 return "";
177 }
178 }
179
180
181
182 /**
183 * Transform an XML document using a XSLT stylesheet,
184 * but using a DOMResult whose node should be set to the Document donated by resultNode
185 */
186 public Node transform_withResultNode(Document stylesheet, Document source, Document resultNode) {
187 return transform(stylesheet, source, null, null, resultNode);
188 }
189
190 public Node transform(Document stylesheet, Document source) {
191 return transform(stylesheet, source, null, null, null);
192 }
193
194 public Node transform(Document stylesheet, Document source, HashMap parameters) {
195 return transform(stylesheet, source, parameters, null, null);
196 }
197
198 public Node transform(Document stylesheet, Document source, HashMap parameters, Document docDocType) {
199 return transform(stylesheet, source, parameters, docDocType, null);
200 }
201
202 protected Node transform(Document stylesheet, Document source, HashMap parameters, Document docDocType, Document resultNode) {
203 try {
204 // Use the TransformerFactory to process the stylesheet Source and generate a Transformer.
205 Transformer transformer = this.t_factory.newTransformer(new DOMSource(stylesheet));
206 logger.info("XMLTransformer transformer is " + transformer);
207 if (parameters != null) {
208 Set params = parameters.entrySet();
209 Iterator i = params.iterator();
210 while (i.hasNext()) {
211 Map.Entry m = (Map.Entry)i.next();
212 transformer.setParameter((String)m.getKey(), m.getValue());
213 }
214 }
215
216 // When we transform the DOMResult, we need to make sure the result of
217 // the transformation has a DocType. For that to happen, we need to create
218 // the DOMResult using a Document with a predefined docType.
219 // If we don't have a DocType then do the transformation with a DOMResult
220 // that does not contain any doctype (like we use to do before).
221 DOMResult result = docDocType == null ? new DOMResult() : new DOMResult(docDocType);
222 if(resultNode != null) {
223 result.setNode(resultNode);
224 }
225 DOMSource domSource = new DOMSource(source);
226 transformer.setErrorListener(new TransformErrorListener(stylesheet, domSource));
227 transformer.transform(domSource, result);
228 return result.getNode(); // pass the entire document
229 }
230 catch (TransformerConfigurationException e) {
231 return transformError("XMLTransformer.transform(Doc, Doc, HashMap, Doc)"
232 + "\ncouldn't create transformer object", e);
233 }
234 catch (TransformerException e) {
235 return transformError("XMLTransformer.transform(Doc, Doc, HashMap, Doc)"
236 + "\ncouldn't transform the source", e);
237 }
238 }
239
240 public Node transform(File stylesheet, File source) {
241 return transform(stylesheet, source, null);
242 }
243
244 // debugAsFile is only to be set to true when either the stylesheet or source parameters
245 // are not objects of type File. The debugAsFile variable is passed into the
246 // TransformErrorListener. When set to true, the TransformErrorListener will itself create
247 // two files containing the stylesheet and source XML, and try to transform the new source
248 // file with the stylesheet file for debugging purposes.
249 protected Node transform(File stylesheet, File source, Document docDocType) {
250 try {
251 Transformer transformer = this.t_factory.newTransformer(new StreamSource(stylesheet));
252 DOMResult result = (docDocType == null) ? new DOMResult() : new DOMResult(docDocType);
253 StreamSource streamSource = new StreamSource(source);
254
255 transformer.setErrorListener(new TransformErrorListener(stylesheet, streamSource));
256
257 transformer.transform(streamSource, result);
258 return result.getNode().getFirstChild();
259 } catch (TransformerConfigurationException e) {
260 return transformError("XMLTransformer.transform(File, File)"
261 + "\ncouldn't create transformer object for files\n"
262 + stylesheet + "\n" + source, e);
263 }
264 catch (TransformerException e) {
265 return transformError("XMLTransformer.transform(File, File)"
266 + "\ncouldn't transform the source for files\n"
267 + stylesheet + "\n" + source, e);
268 }
269 }
270
271 // Given a heading string on the sort of transformation error that occurred and the exception object itself,
272 // this method prints the exception to the tomcat window (system.err) and the greenstone log and then returns
273 // an xhtml error page that is constructed from it.
274 protected Node transformError(String heading, TransformerException e) {
275 String message = heading + "\n" + e.getMessage();
276 logger.error(heading + ": " + e.getMessage());
277
278 String location = e.getLocationAsString();
279 if(location != null) {
280 logger.error(location);
281 message = message + "\n" + location;
282 }
283 System.err.println("****\n" + message + "\n****");
284 return constructErrorXHTMLPage(message);
285 }
286
287 // Given an error message, splits it into separate lines based on any newlines present and generates an xhtml page
288 // (xml Element) with paragraphs for each line. This is then returned so that it can be displayed in the browser.
289 public static Element constructErrorXHTMLPage(String message) {
290 try{
291 String[] lines = message.split("\n");
292
293 Document xhtmlDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
294 // <html></html>
295 Node htmlNode = xhtmlDoc.createElement("html");
296 xhtmlDoc.appendChild(htmlNode);
297 // <head></head>
298 Node headNode = xhtmlDoc.createElement("head");
299 htmlNode.appendChild(headNode);
300 // <title></title>
301 Node titleNode = xhtmlDoc.createElement("title");
302 headNode.appendChild(titleNode);
303 Node titleString = xhtmlDoc.createTextNode("Error occurred");
304 titleNode.appendChild(titleString);
305
306 // <body></body>
307 Node bodyNode = xhtmlDoc.createElement("body");
308 htmlNode.appendChild(bodyNode);
309
310 // finally put the message in the body
311 Node h1Node = xhtmlDoc.createElement("h1");
312 bodyNode.appendChild(h1Node);
313 Node headingString = xhtmlDoc.createTextNode("The following error occurred:");
314 h1Node.appendChild(headingString);
315
316 //Node textNode = xhtmlDoc.createTextNode(message);
317 //bodyNode.appendChild(textNode);
318
319 for (int i = 0; i < lines.length; i++) {
320 Node pNode = xhtmlDoc.createElement("p");
321 Node textNode = xhtmlDoc.createTextNode(lines[i]);
322 pNode.appendChild(textNode);
323 bodyNode.appendChild(pNode);
324 }
325
326 return xhtmlDoc.getDocumentElement();
327
328 }catch(Exception e) {
329 String errmsg = "Exception trying to construct error xhtml page from message: " + message
330 + "\n" + e.getMessage();
331 System.err.println(errmsg);
332 logger.error(errmsg);
333 return null;
334 }
335 }
336
337 // ErrorListener class that can be used to register a handler for any fatal errors, errors and warnings
338 // that may occur when transforming an xml file with an xslt stylesheet using the XMLTransformer.
339 // The errors are printed both to the greenstone.log and to the tomcat console (System.err), and the
340 // error message is stored in the errorMessage variable so that it can be retrieved and be used to
341 // generate an xhtml error page.
342 public class TransformErrorListener implements ErrorListener {
343 protected String errorMessage = null;
344 protected String stylesheet = null;
345 protected Source source = null; // can be DOMSource or StreamSource
346 protected boolean debugAsFile = true; // true if xslt or source are not real physical files
347
348 public TransformErrorListener(String xslt, Source source) {
349 this.stylesheet = xslt;
350 this.source = source;
351 XMLTransformer.debugFileCount++;
352 }
353
354 public TransformErrorListener(Document xslt, Source source) {
355 this.stylesheet = GSXML.elementToString(xslt.getDocumentElement(), true);
356 this.source = source;
357 XMLTransformer.debugFileCount++;
358 }
359
360 public TransformErrorListener(File xslt, Source source) {
361 this.debugAsFile = false; // if this constructor is called, we're dealing with physical files for both xslt and source
362 this.source = source;
363 this.stylesheet = xslt.getAbsolutePath(); // not necessary to get the string from the file
364 // all we were going to do with it *on error* was write it out to a file anyway
365 }
366
367 // Receive notification of a recoverable error.
368 public void error(TransformerException exception) {
369 handleError("Error:\n", exception);
370 }
371 // Receive notification of a non-recoverable error.
372 public void fatalError(TransformerException exception) {
373 handleError("Fatal Error:\n", exception);
374 }
375 // Receive notification of a warning.
376 public void warning(TransformerException exception) {
377 handleError("Warning:\n", exception);
378 }
379
380 public String toString(TransformerException e) {
381 String msg = "Exception encountered was:\n\t";
382 String location = e.getLocationAsString();
383 if(location != null) {
384 msg = msg + "Location: " + location + "\n\t";
385 }
386
387 return msg + "Message: " + e.getMessage();
388 }
389
390 // clears the errorPage variable after the first call to this method
391 public String getErrorMessage() {
392 String errMsg = this.errorMessage;
393 if(this.errorMessage != null) {
394 this.errorMessage = null;
395 }
396 return errMsg;
397 }
398
399 // sets the errorMessage member variable to the data stored in the exception
400 // and writes the errorMessage to the logger and tomcat's System.err
401 protected void handleError(String errorType, TransformerException exception) {
402
403 this.errorMessage = errorType + toString(exception);
404
405 // If either the stylesheet or the source to be transformed with it were not files,
406 // so that the transformation was performed in-memory, then the "location" information
407 // during the error handling (if any) wouldn't have been helpful.
408 // To allow proper debugging, we write both stylesheet and source out as physical files
409 // and perform the same transformation again, so that when a transformation error does
410 // occur, the files are not in-memory but can be viewed, and any location information
411 // for the error given by the ErrorListener will be sensible (instead of the unhelpful
412 // "line#0 column#0 in file://somewhere/dummy.xsl").
413 // Note that if the stylesheet and the source it is to transform were both physical
414 // files to start off with, we will not need to perform the same transformation again
415 // since the error reporting would have provided accurate locations for those.
416 if(debugAsFile) {
417
418 performTransformWithPhysicalFiles(); // will give accurate line numbers
419
420 // No need to print out the current error message (seen in the Else statement below),
421 // as the recursive call to XMLTransformer.transform(File, File, false) in method
422 // performTransformWithPhysicalFiles() will do this for us.
423 }
424 else {
425 // printing out the error message
426 // since !debugAsFile, we are dealing with physical files,
427 // variable stylesheet would have stored the filename instead of contents
428 this.errorMessage = this.errorMessage + "\nstylesheet filename: " + stylesheet;
429
430 this.errorMessage += "\nException CAUSE:\n" + exception.getCause();
431 System.err.println("\n****Error transforming xml:\n" + this.errorMessage + "\n****\n");
432 //System.err.println("Stylesheet was:\n + this.stylesheet + "************END STYLESHEET***********\n\n");
433
434 logger.error(this.errorMessage);
435
436 // now print out the source to a file, and run the stylesheet on it using a transform()
437 // then any error will be referring to one of these two input files.
438 }
439 }
440
441 // This method will redo the transformation that went wrong with *real* files:
442 // it writes out the stylesheet and source XML to files first, then performs the transformation
443 // to get the actual line location of where things went wrong (instead of "line#0 column#0 in dummy.xsl")
444 protected void performTransformWithPhysicalFiles() {
445 File webLogsTmpFolder = new File(GlobalProperties.getGSDL3Home() + File.separator + "logs" + File.separator + "tmp");
446 File styleFile = new File(webLogsTmpFolder + File.separator + "stylesheet" + XMLTransformer.debugFileCount + ".xml");
447 File sourceFile = new File(webLogsTmpFolder + File.separator + "source" + XMLTransformer.debugFileCount + ".xml");
448
449 try {
450 // write stylesheet to a file called stylesheet_systemID in tmp
451 FileWriter styleSheetWriter = new FileWriter(styleFile);
452 styleSheetWriter.write(stylesheet, 0, stylesheet.length());
453 styleSheetWriter.flush();
454 styleSheetWriter.close();
455 } catch(Exception e) {
456 System.err.println("*** Exception when trying to write out stylesheet to " + styleFile.getAbsolutePath());
457 }
458
459 try {
460 FileWriter srcWriter = new FileWriter(sourceFile);
461 String contents = "";
462 if(source instanceof DOMSource) {
463 DOMSource domSource = (DOMSource)source;
464 Document doc = (Document)domSource.getNode();
465 contents = GSXML.elementToString(doc.getDocumentElement(), true);
466 //contents = GSXML.xmlNodeToXMLString(domSource.getNode());
467 } else if (source instanceof StreamSource) {
468 StreamSource streamSource = (StreamSource)source;
469 BufferedReader reader = new BufferedReader(streamSource.getReader());
470 String line = "";
471 while((line = reader.readLine()) != null) {
472 contents = contents + line + "\n";
473 }
474 }
475 srcWriter.write(contents, 0, contents.length());
476 srcWriter.flush();
477 srcWriter.close();
478 } catch(Exception e) {
479 System.err.println("*** Exception when trying to write out stylesheet to " + sourceFile.getAbsolutePath());
480 }
481
482 System.err.println("*****************************************");
483 System.err.println("Look for stylesheet in: " + styleFile.getAbsolutePath());
484 System.err.println("Look for source XML in: " + sourceFile.getAbsolutePath());
485
486 // now perform the transform again, which will assign another TransformErrorListener
487 // but since debuggingAsFile is turned off, we won't recurse into this section of
488 // handling the error again
489 XMLTransformer.this.transform(styleFile, sourceFile); // calls the File, File version, so debugAsFile will be false
490
491 }
492 }
493}
Note: See TracBrowser for help on using the repository browser.