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

Last change on this file since 33711 was 33631, checked in by kjdon, 5 years ago

added a bit more error reporting

  • Property svn:keywords set to Author Date Id Revision
File size: 38.3 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 java.io.BufferedReader;
22import java.io.File;
23import java.io.FileInputStream;
24import java.io.FileNotFoundException;
25import java.io.FileWriter;
26import java.io.InputStreamReader;
27import java.io.IOException;
28import java.io.StringReader;
29import java.io.StringWriter;
30import java.io.UnsupportedEncodingException;
31import java.net.MalformedURLException;
32import java.net.URISyntaxException;
33import java.net.URI;
34import java.net.URL;
35import java.util.HashMap;
36import java.util.Iterator;
37import java.util.Map;
38import java.util.Properties;
39import java.util.Set;
40
41import javax.xml.parsers.DocumentBuilderFactory;
42import javax.xml.transform.ErrorListener;
43import javax.xml.transform.OutputKeys;
44import javax.xml.transform.Source;
45import javax.xml.transform.Transformer;
46import javax.xml.transform.TransformerConfigurationException;
47import javax.xml.transform.TransformerException;
48import javax.xml.transform.TransformerFactory;
49import javax.xml.transform.URIResolver;
50import javax.xml.transform.dom.DOMResult;
51import javax.xml.transform.dom.DOMSource;
52import javax.xml.transform.stream.StreamResult;
53import javax.xml.transform.stream.StreamSource;
54
55import org.apache.log4j.Logger;
56import org.greenstone.util.GlobalProperties;
57import org.w3c.dom.Document;
58import org.w3c.dom.Element;
59import org.w3c.dom.Node;
60
61import org.xml.sax.XMLReader;
62import org.xml.sax.EntityResolver;
63import org.xml.sax.InputSource;
64import org.xml.sax.SAXException;
65import javax.xml.transform.sax.SAXSource;
66import org.xml.sax.helpers.XMLReaderFactory;
67/**
68 * XMLTransformer - utility class for greenstone
69 *
70 * transforms xml using xslt
71 *
72 */
73public class XMLTransformer
74{
75 private static int debugFileCount = 0; // for unique filenames when debugging XML transformations with physical files
76
77 static Logger logger = Logger.getLogger(org.greenstone.gsdl3.util.XMLTransformer.class.getName());
78
79 /** The transformer factory we're using */
80 TransformerFactory t_factory = null;
81
82 /**
83 * The no-arguments constructor.
84 *
85 * Any exceptions thrown are caught internally
86 *
87 * @see javax.xml.transform.TransformerFactory
88 */
89 public XMLTransformer()
90 {
91 // http://download.oracle.com/docs/cd/E17476_01/javase/1.5.0/docs/api/index.html?javax/xml/transform/TransformerFactory.html states that
92 // TransformerFactory.newInstance() looks in jar files for a Factory specified in META-INF/services/javax.xml.transform.TransformerFactory,
93 // else it will use the "platform default"
94 // In this case: xalan.jar's META-INF/services/javax.xml.transform.TransformerFactory contains org.apache.xalan.processor.TransformerFactoryImpl
95 // as required.
96
97 // This means we no longer have to do a System.setProperty("javax.xml.transform.TransformerFactory", "org.apache.xalan.processor.TransformerFactoryImpl");
98 // followed by a this.t_factory = org.apache.xalan.processor.TransformerFactoryImpl.newInstance();
99 // The System.setProperty step to force the TransformerFactory implementation that gets used, conflicts with
100 // Fedora (visiting the Greenstone server pages breaks the Greenstone-tomcat hosted Fedora pages) as Fedora
101 // does not include the xalan.jar and therefore can't then find the xalan TransformerFactory explicitly set.
102
103 // Gone back to forcing use of xalan transformer, since other jars like crimson.jar, which may be on some
104 // classpaths, could be be chosen as the TransformerFactory implementation over xalan. This is what used to
105 // give problems before. Instead, have placed copies of the jars that Fedora needs (xalan.jar and serializer.jar
106 // and the related xsltc.jar which it may need) into packages/tomcat/lib so that it's on the server's classpath
107 // and will be found by Fedora.
108
109 // make sure we are using the xalan transformer
110 System.setProperty("javax.xml.transform.TransformerFactory", "org.apache.xalan.processor.TransformerFactoryImpl");
111 try
112 {
113 this.t_factory = org.apache.xalan.processor.TransformerFactoryImpl.newInstance();
114 //this.t_factory = TransformerFactory.newInstance();
115 this.t_factory.setErrorListener(new TransformErrorListener()); // handle errors in the xml Source used to instantiate transformers
116 }
117 catch (Exception e)
118 {
119 logger.error("exception creating t_factory " + e.getMessage());
120 }
121 }
122
123 /**
124 * Transform an XML document using a XSLT stylesheet
125 *
126 * @param stylesheet
127 * a filename for an XSLT stylesheet
128 * @param xml_in
129 * the XML to be transformed
130 * @return the transformed XML
131 */
132 public String transform(String stylesheet, String xml_in)
133 {
134
135 try
136 {
137 TransformErrorListener transformerErrorListener = (TransformErrorListener) this.t_factory.getErrorListener();
138 transformerErrorListener.setStylesheet(stylesheet);
139 // Use the TransformerFactory to process the stylesheet Source and generate a Transformer.
140 Transformer transformer = this.t_factory.newTransformer(new StreamSource(stylesheet));
141
142 // Use the Transformer to transform an XML Source and send the output to a Result object.
143 StringWriter output = new StringWriter();
144 StreamSource streamSource = new StreamSource(new StringReader(xml_in));
145 transformer.setErrorListener(new TransformErrorListener(stylesheet, streamSource));
146 transformer.transform(streamSource, new StreamResult(output));
147 return output.toString();
148 }
149 catch (TransformerConfigurationException e)
150 {
151 logger.error("couldn't create transformer object: " + e.getMessageAndLocation());
152 logger.error(e.getLocationAsString());
153 return "";
154 }
155 catch (TransformerException e)
156 {
157 logger.error("couldn't transform the source: " + e.getMessageAndLocation());
158 return "";
159 }
160 }
161
162 public String transformToString(Document stylesheet, Document source)
163 {
164 return transformToString(stylesheet, source, null);
165 }
166
167 public String transformToString(Document stylesheet, Document source, HashMap parameters)
168 {
169
170 try
171 {
172 TransformErrorListener transformerErrorListener = (TransformErrorListener) this.t_factory.getErrorListener();
173 transformerErrorListener.setStylesheet(stylesheet);
174 // Use the TransformerFactory to process the stylesheet Source and generate a Transformer.
175 Transformer transformer = this.t_factory.newTransformer(new DOMSource(stylesheet));
176 if (parameters != null)
177 {
178 Set params = parameters.entrySet();
179 Iterator i = params.iterator();
180 while (i.hasNext())
181 {
182 Map.Entry m = (Map.Entry) i.next();
183 transformer.setParameter((String) m.getKey(), m.getValue());
184 }
185 }
186 //transformer.setParameter("page_lang", source.getDocumentElement().getAttribute(GSXML.LANG_ATT));
187
188 // Use the Transformer to transform an XML Source and send the output to a Result object.
189 StringWriter output = new StringWriter();
190 DOMSource domSource = new DOMSource(source);
191
192 transformer.setErrorListener(new TransformErrorListener(stylesheet, domSource));
193 transformer.transform(domSource, new StreamResult(output));
194 return output.toString();
195 }
196 catch (TransformerConfigurationException e)
197 {
198 logger.error("couldn't create transformer object: " + e.getMessageAndLocation());
199 logger.error(e.getLocationAsString());
200 return "";
201 }
202 catch (TransformerException e)
203 {
204 logger.error("couldn't transform the source: " + e.getMessageAndLocation());
205 return "";
206 }
207 }
208
209 /**
210 * Transform an XML document using a XSLT stylesheet, but using a DOMResult
211 * whose node should be set to the Document donated by resultNode
212 */
213 public Node transform_withResultNode(Document stylesheet, Document source, Document resultNode)
214 {
215 return transform(stylesheet, source, null, null, resultNode);
216 }
217
218 public Node transform(Document stylesheet, Document source)
219 {
220 return transform(stylesheet, source, null, null, null);
221 }
222
223 public Node transform(Document stylesheet, Document source, HashMap<String, Object> parameters)
224 {
225 return transform(stylesheet, source, parameters, null, null);
226 }
227
228 public Node transform(Document stylesheet, Document source, HashMap<String, Object> parameters, Document docDocType)
229 {
230 return transform(stylesheet, source, parameters, docDocType, null);
231 }
232
233 // This method will now set the docType in the new document created and returned, if any are specified in the
234 // (merged) stylesheet being applied. The docDocType parameter is therefore no longer necessary nor used by default.
235 protected Node transform(Document stylesheet, Document source, HashMap<String, Object> parameters, Document docDocType, Document resultNode)
236 {
237
238 try
239 {
240 // Use the TransformerFactory to process the stylesheet Source and generate a Transformer.
241 TransformErrorListener transformerErrorListener = (TransformErrorListener) this.t_factory.getErrorListener();
242 transformerErrorListener.setStylesheet(stylesheet);
243 DOMSource xsl_source = new DOMSource(stylesheet);
244 if (xsl_source == null) {
245 logger.error("couldn't load stylesheet into domsource");
246 return transformError("XMLTransformer.transform(Doc, Doc, HashMap, Doc) couldn't load stylesheet into DOMSource", null);
247 }
248 Transformer transformer = this.t_factory.newTransformer(xsl_source);
249 //logger.info("XMLTransformer transformer is " + transformer); //done in ErrorListener
250 if (transformer == null) { // the above is never supposed to return null :-(
251 logger.error("couldn't create transformer");
252 return transformError("XMLTransformer.transform(Doc, Doc, HashMap, Doc) couldn't load stylesheet", null);
253 }
254 if (parameters != null)
255 {
256 Set params = parameters.entrySet();
257 Iterator i = params.iterator();
258 while (i.hasNext())
259 {
260 Map.Entry m = (Map.Entry) i.next();
261 transformer.setParameter((String) m.getKey(), m.getValue());
262 }
263 }
264
265 // When we transform the DOMResult, we need to make sure the result of
266 // the transformation has a DocType. For that to happen, we need to create
267 // the DOMResult using a Document with a predefined docType.
268
269 // When the DOCType is not explicitly specified (default case), the docDocType variable is null
270 // In such a case, the transformer will work out the docType and output method and the rest
271 // from the stylesheet. Better to let the transformer work this out than GS manually aggregating
272 // all xsls being applied and the GS code deciding on which output method and doctype to use.
273
274 //DOMResult result = docDocType == null ? new DOMResult() : new DOMResult(docDocType);
275 DOMResult result = null;
276
277 Properties props = transformer.getOutputProperties();
278 if(docDocType == null) { // default case
279
280 String outputMethod = props.getProperty(OutputKeys.METHOD);
281 if(outputMethod.equals("html")) {
282 String doctype_public = props.getProperty(OutputKeys.DOCTYPE_PUBLIC);
283 String doctype_system = props.getProperty(OutputKeys.DOCTYPE_SYSTEM);
284
285 if(doctype_public == null) {
286 //doctype_public = ""; // or default to PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"?
287 doctype_public = "-//W3C//DTD HTML 4.01 Transitional//EN";
288 }
289 if(doctype_system == null) {
290 //doctype_system = ""; // or default to "http://www.w3.org/TR/html4/loose.dtd"?
291 doctype_system = "http://www.w3.org/TR/html4/loose.dtd";
292 }
293
294 Document docDocTypeFromTransformer = XMLConverter.newDOM(outputMethod, doctype_public, doctype_system);
295 result = new DOMResult(docDocTypeFromTransformer);
296 }
297 // if output method=xml, the <?xml ?> processing method goes missing hereafter, although it
298 // still exists in OutputKeys' VERSION, ENCODING and OMIT_XML_DECLARATION props at this point
299
300 } else { // if document with doctype was already specified (no longer the default case)
301 result = new DOMResult(docDocType);
302 }
303 // At this point if we haven't initialised result yet, set it to an empty DOMResult
304 if(result == null) {
305 result = new DOMResult();
306 }
307
308
309 if (resultNode != null)
310 {
311 result.setNode(resultNode);
312 }
313 DOMSource domSource = new DOMSource(source);
314 transformer.setErrorListener(new TransformErrorListener(stylesheet, domSource));
315 transformer.transform(domSource, result);
316 return result.getNode(); // pass the entire document
317 }
318 catch (TransformerConfigurationException e)
319 {
320 return transformError("XMLTransformer.transform(Doc, Doc, HashMap, Doc)" + "\ncouldn't create transformer object", e);
321 }
322 catch (TransformerException e)
323 {
324 return transformError("XMLTransformer.transform(Doc, Doc, HashMap, Doc)" + "\ncouldn't transform the source", e);
325 }
326 }
327
328 /**
329 When transforming an XML with an XSLT from the command-line, there's two problems if calling
330 XMLTransformer.transform(File xslt, File xml):
331
332 1. Need to run the transformation process from the location of the stylesheet, else it can't find
333 the stylesheet in order to load it. This is resolved by setting the SystemID for the stylesheet.
334
335 2. The XSLT stylesheet has &lt;xsl:import&gt; statements importing further XSLTs which are furthermore
336 specified by relative paths, requiring that the XSLT (and perhaps also XML) input file is located
337 in the folder from which the relative paths of the import statements are specified. This is resolved
338 by working out the absolute path of each imported XSLT file and setting their SystemIDs too.
339
340 Without solving these problems, things will only work if we 1. run the transformation from the
341 web/interfaces/default/transform folder and 2. need to have the input XSLT file (and XML?)
342 placed in the web/interfacces/default/transform folder too
343
344 Both parts of the solution are described in:
345 http://stackoverflow.com/questions/3699860/resolving-relative-paths-when-loading-xslt-filse
346
347 After the systemID on the XSLT input file is set, we can run the command from the toplevel GS3
348 folder.
349 After resolving the URIs of the XSLT files imported by the input XSLT, by setting their systemIDs,
350 the input XSLT (and XML) file need not be located in web/interfaces/default/transform anymore.
351 */
352 public Node transform(File stylesheet, File source, String interfaceName, Document docDocType)
353 {
354 try
355 {
356 TransformErrorListener transformerErrorListener = (TransformErrorListener) this.t_factory.getErrorListener();
357 transformerErrorListener.setStylesheet(stylesheet);
358
359 // http://stackoverflow.com/questions/3699860/resolving-relative-paths-when-loading-xslt-files
360 Source xsltSource = new StreamSource(new InputStreamReader(new FileInputStream(stylesheet), "UTF-8"));
361 URI systemID = stylesheet.toURI();
362 try {
363 URL url = systemID.toURL();
364 xsltSource.setSystemId(url.toExternalForm());
365 } catch (MalformedURLException mue) {
366 logger.error("Warning: Unable to set systemID for stylesheet to " + systemID);
367 logger.error("Got exception: " + mue.getMessage(), mue);
368 }
369
370 this.t_factory.setURIResolver(new ClasspathResourceURIResolver(interfaceName));
371 Transformer transformer = this.t_factory.newTransformer(xsltSource);
372 // Alternative way of instantiating a newTransformer():
373 //Templates cachedXSLT = this.t_factory.newTemplates(xsltSource);
374 //Transformer transformer = cachedXSLT.newTransformer();
375
376 DOMResult result = (docDocType == null) ? new DOMResult() : new DOMResult(docDocType);
377 StreamSource streamSource = new StreamSource(new InputStreamReader(new FileInputStream(source), "UTF-8"));
378
379 transformer.setErrorListener(new TransformErrorListener(stylesheet, streamSource));
380
381 transformer.transform(streamSource, result);
382 return result.getNode().getFirstChild();
383 }
384 catch (TransformerConfigurationException e)
385 {
386 return transformError("XMLTransformer.transform(File, File)" + "\ncouldn't create transformer object for files\n" + stylesheet + "\n" + source, e);
387 }
388 catch (TransformerException e)
389 {
390 return transformError("XMLTransformer.transform(File, File)" + "\ncouldn't transform the source for files\n" + stylesheet + "\n" + source, e);
391 }
392 catch (UnsupportedEncodingException e)
393 {
394 return transformError("XMLTransformer.transform(File, File)" + "\ncouldn't read file due to an unsupported encoding\n" + stylesheet + "\n" + source, e);
395 }
396 catch (FileNotFoundException e)
397 {
398 return transformError("XMLTransformer.transform(File, File)" + "\ncouldn't find the file specified\n" + stylesheet + "\n" + source, e);
399 }
400 }
401
402 /**
403 * Class for resolving the relative paths used for &lt;xsl:import&gt;s
404 * when transforming XML with an XSLT
405 */
406 class ClasspathResourceURIResolver implements URIResolver {
407 String interface_name;
408
409 public ClasspathResourceURIResolver(String interfaceName) {
410 interface_name = interfaceName;
411 }
412
413 /**
414 * Override the URIResolver.resolve() method to turn relative paths of imported xslt files into
415 * absolute paths and set these absolute paths as their SystemIDs when loading them into memory.
416 * @see http://stackoverflow.com/questions/3699860/resolving-relative-paths-when-loading-xslt-files
417 */
418
419 @Override
420 public Source resolve(String href, String base) throws TransformerException {
421
422 //System.err.println("href: " + href); // e.g. href: layouts/main.xsl
423 //System.err.println("base: " + base); // e.g for *toplevel* base: file:/path/to/gs3-svn/CL1.xslt
424
425
426 // 1. for any xsl imported by the original stylesheet, try to work out its absolute path location
427 // by concatenating the parent folder of the base stylesheet file with the href of the file it
428 // imports (which is a path relative to the base file). This need not hold true for the very 1st
429 // base stylesheet: it may be located somewhere entirely different from the greenstone XSLT
430 // files it refers to. This is the case when running transforms on the commandline for testing
431
432 File importXSLfile = new File(href); // assume the xsl imported has an absolute path
433
434 if(!importXSLfile.isAbsolute()) { // imported xsl specifies a path relative to parent of base
435 try {
436 URI baseURI = new URI(base);
437 importXSLfile = new File(new File(baseURI).getParent(), href);
438
439 if(!importXSLfile.exists()) { // if the imported file does not exist, it's not
440 // relative to the base (the stylesheet file importing it). So look in the
441 // interface's transform subfolder for the xsl file to be imported
442 importXSLfile = new File(GSFile.interfaceStylesheetFile(GlobalProperties.getGSDL3Home(), interface_name, href));
443 }
444 } catch(URISyntaxException use) {
445 importXSLfile = new File(href); // try
446 }
447 }
448
449 String importXSLfilepath = ""; // for printing the expected file path on error
450 try {
451 // path of import XSL file after resolving any .. and . dir paths
452 importXSLfilepath = importXSLfile.getCanonicalPath();
453 } catch(IOException ioe) { // resort to using the absolute path
454 importXSLfilepath = importXSLfile.getAbsolutePath();
455 }
456
457 // 2. now we know where the XSL file being imported lives, so set the systemID to its
458 // absolute path when loading it into a Source object
459 URI systemID = importXSLfile.toURI();
460 Source importXSLsrc = null;
461 try {
462 importXSLsrc = new StreamSource(new InputStreamReader(new FileInputStream(importXSLfile), "UTF-8"));
463 URL url = systemID.toURL();
464 importXSLsrc.setSystemId(url.toExternalForm());
465 } catch (MalformedURLException mue) {
466 logger.error("Warning: Unable to set systemID for imported stylesheet to " + systemID);
467 logger.error("Got exception: " + mue.getMessage(), mue);
468 } catch(FileNotFoundException fne) {
469 logger.error("ERROR: importXSLsrc file does not exist " + importXSLfilepath);
470 logger.error("\tError msg: " + fne.getMessage(), fne);
471 } catch(UnsupportedEncodingException uee) {
472 logger.error("ERROR: could not resolve relative path of import XSL file " + importXSLfilepath
473 + " because of encoding issues: " + uee.getMessage(), uee);
474 }
475 return importXSLsrc;
476 //return new StreamSource(this.getClass().getClassLoader().getResourceAsStream(href)); // won't work
477 }
478 }
479
480 public Node transform(File stylesheet, File source)
481 {
482 return transform(stylesheet, source, null);
483 }
484
485 /** transform method for when the source doc may contain DTD references. eg in gberg collection - the files are kind of outside of greenstone.
486 * pass in an entity resolver, and it will be used by the XMLReader */
487 public Node transform(File stylesheet, File source, Document docDocType, EntityResolver er)
488 {
489 try
490 {
491 TransformErrorListener transformerErrorListener = (TransformErrorListener) this.t_factory.getErrorListener();
492 transformerErrorListener.setStylesheet(stylesheet);
493 Transformer transformer = this.t_factory.newTransformer(new StreamSource(new InputStreamReader(new FileInputStream(stylesheet), "UTF-8")));
494 DOMResult result = (docDocType == null) ? new DOMResult() : new DOMResult(docDocType);
495
496 XMLReader reader = XMLReaderFactory.createXMLReader();
497 reader.setEntityResolver(er);
498 SAXSource xmlSource = new SAXSource(reader, new InputSource(new InputStreamReader(new FileInputStream(source), "UTF-8")));
499
500 transformer.setErrorListener(new TransformErrorListener(stylesheet, xmlSource));
501
502 transformer.transform(xmlSource, result);
503 return result.getNode().getFirstChild();
504 }
505 catch (SAXException e)
506 {
507 return transformError("XMLTransformer.transform(File, File), couldn't create XMLReader for source\n"+ source, e);
508 }
509 catch (TransformerConfigurationException e)
510 {
511 return transformError("XMLTransformer.transform(File, File)" + "\ncouldn't create transformer object for files\n" + stylesheet + "\n" + source, e);
512 }
513 catch (TransformerException e)
514 {
515 return transformError("XMLTransformer.transform(File, File)" + "\ncouldn't transform the source for files\n" + stylesheet + "\n" + source, e);
516 }
517 catch (UnsupportedEncodingException e)
518 {
519 return transformError("XMLTransformer.transform(File, File)" + "\ncouldn't read file due to an unsupported encoding\n" + stylesheet + "\n" + source, e);
520 }
521 catch (FileNotFoundException e)
522 {
523 return transformError("XMLTransformer.transform(File, File)" + "\ncouldn't find the file specified\n" + stylesheet + "\n" + source, e);
524 }
525 }
526 // debugAsFile is only to be set to true when either the stylesheet or source parameters
527 // are not objects of type File. The debugAsFile variable is passed into the
528 // TransformErrorListener. When set to true, the TransformErrorListener will itself create
529 // two files containing the stylesheet and source XML, and try to transform the new source
530 // file with the stylesheet file for debugging purposes.
531 protected Node transform(File stylesheet, File source, Document docDocType)
532 {
533 try
534 {
535 TransformErrorListener transformerErrorListener = (TransformErrorListener) this.t_factory.getErrorListener();
536 transformerErrorListener.setStylesheet(stylesheet);
537 Transformer transformer = this.t_factory.newTransformer(new StreamSource(new InputStreamReader(new FileInputStream(stylesheet), "UTF-8")));
538 DOMResult result = (docDocType == null) ? new DOMResult() : new DOMResult(docDocType);
539 StreamSource streamSource = new StreamSource(new InputStreamReader(new FileInputStream(source), "UTF-8"));
540
541 transformer.setErrorListener(new TransformErrorListener(stylesheet, streamSource));
542
543 transformer.transform(streamSource, result);
544 return result.getNode().getFirstChild();
545 }
546 catch (TransformerConfigurationException e)
547 {
548 return transformError("XMLTransformer.transform(File, File)" + "\ncouldn't create transformer object for files\n" + stylesheet + "\n" + source, e);
549 }
550 catch (TransformerException e)
551 {
552 return transformError("XMLTransformer.transform(File, File)" + "\ncouldn't transform the source for files\n" + stylesheet + "\n" + source, e);
553 }
554 catch (UnsupportedEncodingException e)
555 {
556 return transformError("XMLTransformer.transform(File, File)" + "\ncouldn't read file due to an unsupported encoding\n" + stylesheet + "\n" + source, e);
557 }
558 catch (FileNotFoundException e)
559 {
560 return transformError("XMLTransformer.transform(File, File)" + "\ncouldn't find the file specified\n" + stylesheet + "\n" + source, e);
561 }
562 }
563
564 // Given a heading string on the sort of transformation error that occurred and the exception object itself,
565 // this method prints the exception to the tomcat window (system.err) and the greenstone log and then returns
566 // an xhtml error page that is constructed from it.
567 protected Node transformError(String heading, Exception e)
568 {
569 if (e == null) {
570 return constructErrorXHTMLPage(heading);
571 }
572 String message = heading + "\n" + e.getMessage();
573 logger.error(heading + ": " + e.getMessage());
574
575 if (e instanceof TransformerException)
576 {
577 String location = ((TransformerException) e).getLocationAsString();
578 if (location != null)
579 {
580 logger.error(location);
581 message = message + "\n" + location;
582 }
583 }
584 System.err.println("****\n" + message + "\n****");
585 return constructErrorXHTMLPage(message);
586 }
587
588 // Given an error message, splits it into separate lines based on any newlines present and generates an xhtml page
589 // (xml Element) with paragraphs for each line. This is then returned so that it can be displayed in the browser.
590 public static Element constructErrorXHTMLPage(String message)
591 {
592 try
593 {
594 String[] lines = message.split("\n");
595
596 Document xhtmlDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
597 // <html></html>
598 Node htmlNode = xhtmlDoc.createElement("html");
599 xhtmlDoc.appendChild(htmlNode);
600 // <head></head>
601 Node headNode = xhtmlDoc.createElement("head");
602 htmlNode.appendChild(headNode);
603 // <title></title>
604 Node titleNode = xhtmlDoc.createElement("title");
605 headNode.appendChild(titleNode);
606 Node titleString = xhtmlDoc.createTextNode("Error occurred");
607 titleNode.appendChild(titleString);
608
609 // <body></body>
610 Node bodyNode = xhtmlDoc.createElement("body");
611 htmlNode.appendChild(bodyNode);
612
613 // finally put the message in the body
614 Node h1Node = xhtmlDoc.createElement("h1");
615 bodyNode.appendChild(h1Node);
616 Node headingString = xhtmlDoc.createTextNode("The following error occurred:");
617 h1Node.appendChild(headingString);
618
619 //Node textNode = xhtmlDoc.createTextNode(message);
620 //bodyNode.appendChild(textNode);
621
622 for (int i = 0; i < lines.length; i++)
623 {
624 Node pNode = xhtmlDoc.createElement("p");
625 Node textNode = xhtmlDoc.createTextNode(lines[i]);
626 pNode.appendChild(textNode);
627 bodyNode.appendChild(pNode);
628 }
629
630 return xhtmlDoc.getDocumentElement();
631
632 }
633 catch (Exception e)
634 {
635 String errmsg = "Exception trying to construct error xhtml page from message: " + message + "\n" + e.getMessage();
636 System.err.println(errmsg);
637 logger.error(errmsg);
638 return null;
639 }
640 }
641
642 // ErrorListener class for both Transformer objects and TransformerFactory objects.
643 // This class can be used to register a handler for any fatal errors, errors and warnings that
644 // may occur when either transforming an xml file with an xslt stylesheet using the XMLTransformer,
645 // or when instantiating a Transformer object using the XMLTransformer's TransformerFactory member var.
646 // The latter case occurs when the xml Source used to instantiate a Transformer from a TransformerFactory
647 // is invalid in some manner, which results in a null Transformer object. However, as no
648 // TransformerConfigurationException or TransformerException are thrown in this case, the errors
649 // would have not been noticed until things go wrong later when trying to use the (null) Transformer.
650 //
651 // The errors caught by this ErrorListener class are printed both to the greenstone.log and to the
652 // tomcat console (System.err), and the error message is stored in the errorMessage variable so that
653 // it can be retrieved and be used to generate an xhtml error page.
654 public class TransformErrorListener implements ErrorListener
655 {
656 protected String errorMessage = null;
657 protected String stylesheet = null;
658 protected Source source = null; // can be DOMSource or StreamSource
659 protected boolean debugAsFile = true; // true if xslt or source are not real physical files
660
661 // *********** METHODS TO BE CALLED WHEN SETTING AN ERROR LISTENER ON TRANSFORMERFACTORY OBJECTS
662 // The default constructor is only for when setting an ErrorListener on TransformerFactory objects
663 public TransformErrorListener()
664 {
665 this.stylesheet = null;
666 this.source = null;
667 XMLTransformer.debugFileCount++;
668 }
669
670 public void setStylesheet(Document xslt)
671 {
672 this.debugAsFile = true;
673 this.stylesheet = GSXML.elementToString(xslt.getDocumentElement(), true);
674 this.source = null;
675 }
676
677 public void setStylesheet(String xslt)
678 {
679 this.debugAsFile = true;
680 this.stylesheet = xslt;
681 this.source = null;
682 }
683
684 public void setStylesheet(File xslt)
685 {
686 this.debugAsFile = false; // if this constructor is called, we're dealing with physical files for both xslt and source
687 this.stylesheet = xslt.getAbsolutePath();
688 this.source = null;
689 }
690
691 // *********** METHODS TO BE CALLED WHEN SETTING AN ERROR LISTENER ON TRANSFORMERFACTORY OBJECTS
692 // When setting an ErrorListener on Transformer object, the ErrorListener takes a Stylesheet xslt and a Source
693 public TransformErrorListener(String xslt, Source source)
694 {
695 this.stylesheet = xslt;
696 this.source = source;
697 XMLTransformer.debugFileCount++;
698 }
699
700 public TransformErrorListener(Document xslt, Source source)
701 {
702 this.stylesheet = GSXML.elementToString(xslt.getDocumentElement(), true);
703 this.source = source;
704 XMLTransformer.debugFileCount++;
705 }
706
707 public TransformErrorListener(File xslt, Source source)
708 {
709 this.debugAsFile = false; // if this constructor is called, we're dealing with physical files for both xslt and source
710 this.source = source;
711 this.stylesheet = xslt.getAbsolutePath(); // not necessary to get the string from the file
712 // all we were going to do with it *on error* was write it out to a file anyway
713 }
714
715 // *********** METHODS CALLED AUTOMATICALLY ON ERROR
716
717 // Receive notification of a recoverable error.
718 public void error(TransformerException exception)
719 {
720 handleError("Error:\n", exception);
721 }
722
723 // Receive notification of a non-recoverable error.
724 public void fatalError(TransformerException exception)
725 {
726 handleError("Fatal Error:\n", exception);
727 }
728
729 // Receive notification of a warning.
730 public void warning(TransformerException exception)
731 {
732 handleError("Warning:\n", exception);
733 }
734
735 public String toString(TransformerException e)
736 {
737 String msg = "Exception encountered was:\n\t";
738 String location = e.getLocationAsString();
739 if (location != null)
740 {
741 msg = msg + "Location: " + location + "\n\t";
742 }
743
744 return msg + "Message: " + e.getMessage();
745 }
746
747 // clears the errorPage variable after the first call to this method
748 public String getErrorMessage()
749 {
750 String errMsg = this.errorMessage;
751 if (this.errorMessage != null)
752 {
753 this.errorMessage = null;
754 }
755 return errMsg;
756 }
757
758 // sets the errorMessage member variable to the data stored in the exception
759 // and writes the errorMessage to the logger and tomcat's System.err
760 protected void handleError(String errorType, TransformerException exception)
761 {
762
763 this.errorMessage = errorType + toString(exception);
764
765 // If either the stylesheet or the source to be transformed with it were not files,
766 // so that the transformation was performed in-memory, then the "location" information
767 // during the error handling (if any) wouldn't have been helpful.
768 // To allow proper debugging, we write both stylesheet and source out as physical files
769 // and perform the same transformation again, so that when a transformation error does
770 // occur, the files are not in-memory but can be viewed, and any location information
771 // for the error given by the ErrorListener will be sensible (instead of the unhelpful
772 // "line#0 column#0 in file://somewhere/dummy.xsl").
773 // Note that if the stylesheet and the source it is to transform were both physical
774 // files to start off with, we will not need to perform the same transformation again
775 // since the error reporting would have provided accurate locations for those.
776 if (debugAsFile)
777 {
778 System.err.println("\n****Original error transforming xml:\n" + this.errorMessage + "\n****\n");
779 System.err.println("***** About to perform the transform again with actual files\n******\n");
780 logger.error("Original transformation error: " + this.errorMessage);
781 logger.error("**** About to perform transform again with actual files");
782
783
784 performTransformWithPhysicalFiles(); // will give accurate line numbers
785
786 // No need to print out the current error message (seen in the Else statement below),
787 // as the recursive call to XMLTransformer.transform(File, File, false) in method
788 // performTransformWithPhysicalFiles() will do this for us.
789 }
790 else
791 {
792 // printing out the error message
793 // since !debugAsFile, we are dealing with physical files,
794 // variable stylesheet would have stored the filename instead of contents
795 this.errorMessage = this.errorMessage + "\nstylesheet filename: " + stylesheet;
796
797 this.errorMessage += "\nException CAUSE:\n" + exception.getCause();
798 System.err.println("\n****Error transforming xml:\n" + this.errorMessage + "\n****\n");
799 //System.err.println("Stylesheet was:\n + this.stylesheet + "************END STYLESHEET***********\n\n");
800
801 logger.error(this.errorMessage);
802
803 // now print out the source to a file, and run the stylesheet on it using a transform()
804 // then any error will be referring to one of these two input files.
805 }
806 }
807
808 // This method will redo the transformation that went wrong with *real* files:
809 // it writes out the stylesheet and source XML to files first, then performs the transformation
810 // to get the actual line location of where things went wrong (instead of "line#0 column#0 in dummy.xsl")
811 protected void performTransformWithPhysicalFiles()
812 {
813 File webLogsTmpFolder = new File(GlobalProperties.getGSDL3Home() + File.separator + "logs" + File.separator + "tmp");
814 if (!webLogsTmpFolder.exists())
815 {
816 webLogsTmpFolder.mkdirs(); // create any necessary folders
817 }
818 File styleFile = new File(webLogsTmpFolder + File.separator + "stylesheet" + XMLTransformer.debugFileCount + ".xml");
819 File sourceFile = new File(webLogsTmpFolder + File.separator + "source" + XMLTransformer.debugFileCount + ".xml");
820 try
821 {
822 // write stylesheet to a file called stylesheet_systemID in tmp
823 FileWriter styleSheetWriter = new FileWriter(styleFile);
824 styleSheetWriter.write(stylesheet, 0, stylesheet.length());
825 styleSheetWriter.flush();
826 styleSheetWriter.close();
827 }
828 catch (Exception e)
829 {
830 System.err.println("*** Exception when trying to write out stylesheet to " + styleFile.getAbsolutePath());
831 }
832
833 if (this.source != null)
834 { // ErrorListener was set on a Transformer object
835 try
836 {
837 FileWriter srcWriter = new FileWriter(sourceFile);
838 String contents = "";
839 if (source instanceof DOMSource)
840 {
841 DOMSource domSource = (DOMSource) source;
842 Document doc = (Document) domSource.getNode();
843 contents = GSXML.elementToString(doc.getDocumentElement(), true);
844 //contents = GSXML.xmlNodeToXMLString(domSource.getNode());
845 }
846 else if (source instanceof StreamSource)
847 {
848 StreamSource streamSource = (StreamSource) source;
849 BufferedReader reader = new BufferedReader(streamSource.getReader());
850 String line = "";
851 while ((line = reader.readLine()) != null)
852 {
853 contents = contents + line + "\n";
854 }
855 }
856 srcWriter.write(contents, 0, contents.length());
857 srcWriter.flush();
858 srcWriter.close();
859 }
860 catch (Exception e)
861 {
862 System.err.println("*** Exception when trying to write out stylesheet to " + sourceFile.getAbsolutePath());
863 }
864 }
865
866 System.err.println("*****************************************");
867 System.err.println("Look for stylesheet in: " + styleFile.getAbsolutePath());
868 if (this.source != null)
869 { // ErrorListener was set on a Transformer object
870 System.err.println("Look for source XML in: " + sourceFile.getAbsolutePath());
871 }
872
873 // now perform the transform again, which will assign another TransformErrorListener
874 // but since debuggingAsFile is turned off, we won't recurse into this section of
875 // handling the error again
876 if (this.source != null)
877 { // ErrorListener was set on a Transformer object
878 XMLTransformer.this.transform(styleFile, sourceFile); // calls the File, File version, so debugAsFile will be false
879 }
880 else
881 { // ErrorListener was set on a TransformerFactory object
882
883 // The recursive step in this case is to perform the instantiation
884 // of the Transformer object again.
885 // Only one TransformerFactory object per XMLTransformer,
886 // and only one TransformerHandler object set on any TransformerFactory
887 // But the stylesheet used to create a Transformer from that TransformerFactory
888 // object changes each time, by calls to setStylesheet(),
889 // Therefore, the debugAsFile state for the single TransformerFactory's
890 // TransformerHandler changes each time also.
891
892 try
893 {
894 debugAsFile = false;
895 this.stylesheet = styleFile.getAbsolutePath();
896 //TransformErrorListener transformerErrorListener = (TransformErrorListener)XMLTransformer.this.t_factory.getErrorListener();
897 //transformerErrorListener.setStylesheet(styleFile);
898 Transformer transformer = XMLTransformer.this.t_factory.newTransformer(new StreamSource(styleFile));
899 if (transformer == null)
900 {
901 String msg = "XMLTransformer transformer is " + transformer;
902 logger.info(msg);
903 System.out.println(msg + "\n****\n");
904 }
905 }
906 catch (TransformerConfigurationException e)
907 {
908 String message = "Couldn't create transformer object: " + e.getMessageAndLocation();
909 logger.error(message);
910 logger.error(e.getLocationAsString());
911 System.out.println(message);
912 }
913 }
914 }
915 }
916}
Note: See TracBrowser for help on using the repository browser.