Merge remote-tracking branch 'origin/5.4'
[scilab.git] / scilab / modules / helptools / src / java / org / scilab / modules / helptools / DocbookTagConverter.java
1 /*
2  * Scilab ( http://www.scilab.org/ ) - This file is part of Scilab
3  * Copyright (C) 2010 - Calixte DENIZET
4  *
5  * This file must be used under the terms of the CeCILL.
6  * This source file is licensed as described in the file COPYING, which
7  * you should have received as part of this distribution.  The terms
8  * are also available at
9  * http://www.cecill.info/licences/Licence_CeCILL_V2-en.txt
10  *
11  */
12
13 package org.scilab.modules.helptools;
14
15 import java.io.File;
16 import java.io.IOException;
17 import java.lang.reflect.InvocationTargetException;
18 import java.lang.reflect.Method;
19 import java.util.ArrayList;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Stack;
24
25 import javax.xml.parsers.ParserConfigurationException;
26 import javax.xml.parsers.SAXParser;
27 import javax.xml.parsers.SAXParserFactory;
28
29 import org.xml.sax.Attributes;
30 import org.xml.sax.InputSource;
31 import org.xml.sax.Locator;
32 import org.xml.sax.SAXException;
33 import org.xml.sax.helpers.DefaultHandler;
34
35 import org.scilab.modules.helptools.external.ExternalXMLHandler;
36 import org.scilab.modules.helptools.external.HTMLMathMLHandler;
37 import org.scilab.modules.helptools.external.HTMLScilabHandler;
38 import org.scilab.modules.helptools.external.HTMLSVGHandler;
39
40 /**
41  * Class the convert a DocBook xml file
42  * @author Calixte DENIZET
43  */
44 public class DocbookTagConverter extends DefaultHandler {
45
46     private static final String DOCBOOKURI = "http://docbook.org/ns/docbook";
47     private static final Class[] argsType = new Class[] {Map.class, String.class};
48
49     private Map<String, Method> mapMeth = new HashMap<String, Method>();
50     private Map<String, ExternalXMLHandler> externalHandlers = new HashMap<String, ExternalXMLHandler>();
51     private List<DocbookTagConverter> converters;
52     private final File in;
53     private DocbookElement baseElement = new DocbookElement(null, null, null);
54     private Stack<DocbookElement> stack = new Stack<DocbookElement>();
55     private String errors = "";
56
57     /**
58      * True when an error is met during the parsing
59      */
60     protected boolean hasError;
61
62     /**
63      * The file which is parsed
64      */
65     protected String currentFileName;
66
67     /**
68      * The file which is parsed
69      */
70     protected String currentBaseName;
71
72     /**
73      * Useful to locate the errors
74      */
75     protected Locator locator;
76
77     /**
78      * Constructor
79      * @param in the input file path
80      */
81     public DocbookTagConverter(String in) throws IOException {
82         if (in != null && !in.isEmpty()) {
83             this.in = new File(in);
84         } else {
85             this.in = null;
86         }
87     }
88
89     /**
90      * Constructor
91      * @param in the inputstream
92      * @param inName the name of the input file
93      */
94     public DocbookTagConverter(String inName, DocbookElement elem) throws IOException {
95         this(inName);
96         baseElement = elem;
97     }
98
99     /**
100      * Handle the tag
101      * @param tag the tag to handle
102      * @param attributes the attributes as a Map
103      * @param contents the contents between the matching tags
104      * @return the HTML code
105      * @throws UnhandledDocbookTagException if an handled tga is encountered
106      */
107     public String handleDocbookTag(String tag, Map attributes, String contents) throws SAXException {
108         if (tag != null && tag.length() > 0) {
109             Method method = mapMeth.get(tag);
110             if (method == null) {
111                 String name = "handle" + Character.toString(Character.toUpperCase(tag.charAt(0))) + tag.substring(1);
112                 try {
113                     method = this.getClass().getMethod(name, argsType);
114                     mapMeth.put(tag, method);
115                 } catch (NoSuchMethodException e) {
116                     throw new UnhandledDocbookTagException(tag);
117                 }
118             }
119             try {
120                 return (String) method.invoke(this, new Object[] {attributes, contents});
121             } catch (IllegalAccessException e) {
122                 throw new UnhandledDocbookTagException(tag);
123             } catch (InvocationTargetException e) {
124                 throw new SAXException("Problem with tag " + tag + "\n" + e.getCause());
125             }
126         } else {
127             throw new UnhandledDocbookTagException(tag);
128         }
129     }
130
131     public String getCurrentFileName() {
132         return currentFileName;
133     }
134
135     public String getCurrentBaseName() {
136         return currentBaseName;
137     }
138
139     /**
140      * Register an Docbook tag converter. The aim is to parse the xml one time.
141      * @param c the converter to register
142      */
143     public void registerConverter(DocbookTagConverter c) {
144         if (converters == null) {
145             converters = new ArrayList<DocbookTagConverter>();
146         }
147
148         converters.add(c);
149         c.setDocumentLocator(locator);
150     }
151
152     /**
153      * @param c the converter to remove
154      */
155     public void removeConverter(DocbookTagConverter c) {
156         if (converters != null) {
157             converters.remove(c);
158         }
159     }
160
161     /**
162      * @return all the registered converters
163      */
164     public DocbookTagConverter[] getConverters() {
165         if (converters == null) {
166             return null;
167         }
168
169         return converters.toArray(new DocbookTagConverter[0]);
170     }
171
172     /**
173      * Register an XMLHandler for external XML
174      * @param h the external XML handler
175      */
176     public void registerExternalXMLHandler(ExternalXMLHandler h) {
177         if (externalHandlers.get(h.getURI()) == null) {
178             externalHandlers.put(h.getURI(), h);
179             h.setConverter(this);
180         }
181     }
182
183     /**
184      * @param tagName the tag name
185      * @return true if the contents of the tag must be escaped
186      */
187     public boolean isEscapable(String tagName, String uri) {
188         return true;
189     }
190
191     /**
192      * @param tagName the tag name
193      * @return true if the contents of the tag must be trimed
194      */
195     public boolean isTrimable(String tagName) {
196         return true;
197     }
198
199     /**
200      * @return the parent tag name
201      */
202     public String getParentTagName() {
203         return stack.peek().getName();
204     }
205
206     /**
207      * @return the parent tag contents
208      */
209     public String getParentContent() {
210         return stack.peek().getStringBuilder().toString();
211     }
212
213     /**
214      * Start the conversion
215      * @throws SAXException if a problem is encountered during the parsing
216      * @throws IOException if an IO problem is encountered
217      */
218     public void convert() throws SAXException, IOException {
219         SAXParserFactory factory = SAXParserFactory.newInstance();
220         factory.setValidating(false);
221         factory.setNamespaceAware(true);
222
223         try {
224             factory.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
225             SAXParser parser = factory.newSAXParser();
226             // Must be uncommented to be able to read comments
227             //parser.setProperty("http://xml.org/sax/properties/lexical-handler", this);
228             parser.parse(in, this);
229         } catch (ParserConfigurationException e) {
230             exceptionOccurred(e);
231         } catch (SAXException e) {
232             System.err.println(e);
233         } catch (IOException e) {
234             System.err.println(e);
235         }
236
237         if (hasError) {
238             throw new SAXException(errors);
239         }
240     }
241
242     /**
243      * {@inheritDoc}
244      */
245     public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
246         currentFileName = systemId;
247         currentBaseName = new File(systemId).getName();
248         HTMLMathMLHandler.getInstance().resetCompt();
249         HTMLSVGHandler.getInstance().resetCompt();
250         HTMLScilabHandler.getInstance().resetCompt();
251         if (converters != null) {
252             for (DocbookTagConverter conv : converters) {
253                 conv.resolveEntity(publicId, systemId);
254             }
255         }
256
257         return super.resolveEntity(publicId, systemId);
258     }
259
260     /**
261      * {@inheritDoc}
262      */
263     public void startDocument() throws SAXException {
264         stack.push(baseElement.getNewInstance(null, null, null));
265         if (converters != null) {
266             for (DocbookTagConverter conv : converters) {
267                 conv.startDocument();
268             }
269         }
270     }
271
272     /**
273      * {@inheritDoc}
274      */
275     public void endDocument() throws SAXException {
276         stack.clear();
277
278         if (converters != null) {
279             for (DocbookTagConverter conv : converters) {
280                 conv.endDocument();
281             }
282         }
283     }
284
285     /**
286      * {@inheritDoc}
287      */
288     public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
289         if (uri.equals(DOCBOOKURI)) {
290             int len = attributes.getLength();
291             Map<String, String> map = new HashMap<String, String>(len);
292             for (int i = 0; i < len; i++) {
293                 map.put(attributes.getLocalName(i), attributes.getValue(i));
294             }
295             stack.push(baseElement.getNewInstance(localName, uri, map));
296         } else {
297             ExternalXMLHandler h = externalHandlers.get(uri);
298             if (h == null) {
299                 exceptionOccurred(new SAXException("uri " + uri + " not handled"));
300                 return;
301             }
302             StringBuilder buf = h.startExternalXML(localName, attributes, locator);
303             if (buf != null) {
304                 DocbookElement elem = baseElement.getNewInstance(localName, uri, null);
305                 elem.setStringBuilder(buf);
306                 stack.push(elem);
307             }
308         }
309
310         if (converters != null) {
311             for (DocbookTagConverter conv : converters) {
312                 conv.startElement(uri, localName, qName, attributes);
313             }
314         }
315     }
316
317     /**
318      * {@inheritDoc}
319      */
320     public void endElement(String uri, String localName, String qName) throws SAXException {
321         if (uri.equals(DOCBOOKURI)) {
322             DocbookElement elem = stack.pop();
323             if (!elem.getName().equals(localName)) {
324                 exceptionOccurred(new SAXException("tag " + elem.getName() + " is closed with tag " + localName));
325                 return;
326             }
327             try {
328                 DocbookElement elemp = stack.peek();
329                 elemp.setParent(elem);
330                 StringBuilder buf = elem.getStringBuilder();
331                 if (isTrimable(elem.getName())) {
332                     buf = trim(buf);
333                 }
334                 String str = handleDocbookTag(elem.getName(), elem.getAttributes(), buf.toString());
335                 if (str != null) {
336                     elemp.getStringBuilder().append(str);
337                 }
338             } catch (SAXException e) {
339                 exceptionOccurred(e);
340                 return;
341             }
342         } else {
343             ExternalXMLHandler h = externalHandlers.get(uri);
344             if (h == null) {
345                 exceptionOccurred(new SAXException("uri " + uri + " not handled"));
346                 return;
347             }
348             String str = h.endExternalXML(localName);
349             if (str != null) {
350                 stack.pop();
351                 stack.peek().getStringBuilder().append(str);
352             }
353         }
354
355         if (converters != null) {
356             for (DocbookTagConverter conv : converters) {
357                 conv.endElement(uri, localName, qName);
358             }
359         }
360     }
361
362     /*public void comment(char[] ch, int start, int length) throws SAXException {
363
364       }*/
365
366     /**
367      * {@inheritDoc}
368      */
369     public void characters(char[] ch, int start, int length) throws SAXException {
370         int end = start + length;
371
372         if (isEscapable(stack.peek().getName(), stack.peek().getURI())) {
373             StringBuilder buf = stack.peek().getStringBuilder();
374             int save = start;
375             for (int i = start; i < end; i++) {
376                 switch (ch[i]) {
377                     case '\'' :
378                         buf.append(ch, save, i - save);
379                         buf.append("&#0039;");
380                         save = i + 1;
381                         break;
382                     case '\"' :
383                         buf.append(ch, save, i - save);
384                         buf.append("&#0034;");
385                         save = i + 1;
386                         break;
387                     case '<' :
388                         buf.append(ch, save, i - save);
389                         buf.append("&lt;");
390                         save = i + 1;
391                         break;
392                     case '>' :
393                         buf.append(ch, save, i - save);
394                         buf.append("&gt;");
395                         save = i + 1;
396                         break;
397                     case '&' :
398                         buf.append(ch, save, i - save);
399                         buf.append("&amp;");
400                         save = i + 1;
401                         break;
402                     default :
403                         break;
404                 }
405             }
406
407             if (save < end) {
408                 buf.append(ch, save, end - save);
409             }
410         } else {
411             stack.peek().getStringBuilder().append(ch, start, length);
412         }
413
414         if (converters != null) {
415             for (DocbookTagConverter conv : converters) {
416                 conv.characters(ch, start, length);
417             }
418         }
419     }
420
421     /**
422      * {@inheritDoc}
423      */
424     public void setDocumentLocator(Locator locator) {
425         this.locator = locator;
426     }
427
428     /**
429      * @return the document locator
430      */
431     public Locator getDocumentLocator() {
432         return locator;
433     }
434
435     /**
436      * @return the used stack
437      */
438     protected Stack<DocbookElement> getStack() {
439         return stack;
440     }
441
442     /**
443      * Handle an exception depending on the presence of a locator
444      * @param e the exception to handle
445      * @throws SAXException if problem
446      */
447     protected void fatalExceptionOccurred(Exception e) throws SAXException {
448         throw new SAXException(errors + "\nFATAL error:\n" + e.getMessage());
449     }
450
451     /**
452      * Handle an exception depending on the presence of a locator
453      * @param e the exception to handle
454      * @throws SAXException if problem
455      */
456     protected void exceptionOccurred(Exception e) {
457         if (!hasError) {
458             hasError = true;
459         }
460         errors += makeErrorMessage(e);
461     }
462
463     private String makeErrorMessage(Exception e) {
464         String sId = "";
465         if (currentFileName != null) {
466             sId = "SystemID:" + currentFileName;
467         }
468
469         String file;
470         try {
471             file = in.getCanonicalPath();
472         } catch (IOException e1) {
473             e1.printStackTrace();
474             file = null;
475         }
476         if (locator != null) {
477             return "\nCannot parse " + file + ":\n" + e.getMessage() + "\n" + sId + " at line " + locator.getLineNumber();
478         } else {
479             return "\nCannot parse " + file + ":\n" + e.getMessage() + "\n" + sId;
480         }
481     }
482
483     /**
484      * @param the StringBuilder to trim
485      * @return a trimed StringBuilder
486      */
487     private static StringBuilder trim(StringBuilder buf) {
488         int start = 0;
489         int i = 0;
490         int end = buf.length();
491         char c;
492         for (; i < end; i++) {
493             c = buf.charAt(i);
494             if (c != ' ' && c != '\t' && c != '\r' && c != '\n') {
495                 break;
496             }
497         }
498         buf.delete(0, i);
499         end = end - i;
500         for (i = end - 1; i >= 0; i--) {
501             c = buf.charAt(i);
502             if (c != ' ' && c != '\t' && c != '\r' && c != '\n') {
503                 break;
504             }
505         }
506         buf.setLength(i + 1);
507
508         return buf;
509     }
510 }