AbstractTranslator.java
001 /*
002  *  AbstractTranslator.java
003  *  Copyright (c) 1998-2008, The University of Sheffield.
004  *
005  *  This code is from the GATE project (http://gate.ac.uk/) and is free
006  *  software licenced under the GNU General Public License version 3. It is
007  *  distributed without any warranty. For more details see COPYING.txt in the
008  *  top level directory (or at http://gatewiki.sf.net/COPYING.txt).
009  *  
010  *  Hamish Cunningham 17th April 2006
011  */
012 
013 package gate.yam.translate;
014 
015 import gate.util.Files;
016 import gate.util.GateException;
017 import gate.util.GateRuntimeException;
018 import gate.util.Out;
019 import gate.yam.IOHandler;
020 import gate.yam.YamFile;
021 import gate.yam.YamPlugin;
022 import gate.yam.parse.*;
023 import org.apache.log4j.Logger;
024 import org.springframework.core.io.UrlResource;
025 import org.springframework.web.util.HtmlUtils;
026 
027 import java.io.IOException;
028 import java.io.StringWriter;
029 import java.io.Writer;
030 import java.net.URI;
031 import java.net.URISyntaxException;
032 import java.util.ArrayList;
033 import java.util.List;
034 import java.util.Map;
035 import java.util.HashMap;
036 
037 /**
038  * This class provides a model for YAM translators. It is intended to be
039  * extended by all translators.
040  @author Hamish Cunningham
041  */
042 abstract public class AbstractTranslator implements TranslationConstants {
043 
044   /** Array mapping node type name to start/end strings. */
045   String[][] constantsTable;
046 
047   /** Array mapping predicate type name to attributes. */
048   Object[][] predicatesTable;
049 
050   /** Tree holding the results of a YAM parse.  */
051   YamParseTree parseTree;
052 
053   /**
054    * Set the tree holding the results of a YAM parse.
055    */
056   public void setParseTree(YamParseTree parseTree) {
057     this.parseTree = parseTree; 
058   }
059 
060   /** Map from constant name to output strings. */
061   Map constantsMap;
062 
063   /** Map from control types to activity status. */
064   Map controlsMap;
065 
066   /** Map from predicate names to attributes. */
067   Map predicatesMap;
068 
069   /** List of footnotes. */
070   List footnotesList = new ArrayList();
071 
072   /** Logger */
073   static Logger log =
074     Logger.getLogger("gate.yam.translate.AbstractTranslator");
075 
076   /**
077    * Get the markup start element of a constant from the constants map.
078    @param id The id of the constant.
079    */
080   String getConstantStart(int id) {
081     return constantsTable[id][CONSTANTSTART];
082   // getConstantStart(int)
083 
084   /**
085    * Get the markup end element of a constant from the constants map.
086    @param id The id of the constant.
087    */
088   String getConstantEnd(int id) {
089     return constantsTable[id][CONSTANTEND];
090   // getConstantEnd(int)
091 
092   /**
093    * Get the markup start element of a constant from the constants map.
094    @param name The name of the constant.
095    */
096   public String getConstantStart(String name) {
097     return ((String[]) constantsMap.get(name))[CONSTANTSTART];
098   // getConstantStart(String)
099 
100   /**
101    * Get the markup end element of a constant from the constants map.
102    @param name The name of the constant.
103    */
104   public String getConstantEnd(String name) {
105     return ((String[]) constantsMap.get(name))[CONSTANTEND];
106   // getConstantEnd(String)
107 
108   /** Get the three-element array describing a constant from the map. */
109   String[] getConstantArray(String name) {
110     return (String[]) constantsMap.get(name);
111   // getConstantArray(String)
112 
113   /** 
114    * Translation. Results are written to the Writer.
115    */
116   public Writer translate() throws GateException {
117     makeContents();
118     makePreamble();
119     printTree(parseTree.getRootNode());
120 
121     return writer;
122   // translate()
123 
124   /** Traverse (and translate) a subtree. */
125   public void printTree(SimpleNode nodethrows GateException {
126     printTree(node, false);
127   // printTree(SimpleNode)
128 
129   /** Traverse (and translate) a subtree. */
130   public void printTree(SimpleNode node, boolean inContents)
131   throws GateException {
132     printTree(node, inContents, null);
133   // printTree(SimpleNode, boolean)
134    
135   /** Traverse (and translate) a subtree. */
136   public void printTree(
137     SimpleNode node, boolean inContents, SimpleNode nextNode
138   throws GateException {
139     // deal with comments
140     Token firstToken = node.getFirstToken();
141     Token special = null;
142     boolean singleLine = false;
143     if(firstToken != null)
144       special = firstToken.specialToken;
145     StringBuilder commentImage = new StringBuilder();
146     while(special != null) {
147       String si = special.image;
148       if(si.startsWith("%%")) {
149         si = si.substring(2);
150         singleLine = true;
151       else if(si.startsWith("%/*"))
152         si = si.substring(3);
153       else if(si.startsWith("%*/"))
154         si = si.substring(3);
155       commentImage.insert(0, si);
156       special = special.specialToken;
157     }
158     if(commentImage.length() != 0) {
159       String nl = "";
160       if(singleLinenl = "\n";
161       pr(
162         getConstantStart("comment"+ commentImage +
163         getConstantEnd("comment"+ nl
164       );
165       if(firstToken != null)
166         firstToken.specialToken = null;
167     }
168 
169     // node types requiring special processing
170     if(node instanceof ASTControl) {
171       processControl(node);
172       return;
173     else if(node instanceof ASTTargetControl) {
174       String type = node.getFirstToken().image;
175       pr(getConstantStart(type));
176       return;
177     else if(node instanceof ASTAnchor && inContents) {
178       return// in contents tables don't print anchors
179     else if(node instanceof ASTUrl || node instanceof ASTAnchor) {
180       processURLs(node);
181       return;
182     else if(node instanceof ASTPredicate) {
183       String start = node.getStart();
184       if(start == null || ! start.startsWith("include"))
185         processPredicate(node);
186       else
187         node.setStart("");
188     }
189 
190     // before, markup, start
191     String s;
192     if( (s = node.getBefore()) != null pr(s);
193     pr(getConstantStart(node.getId()));
194     if( (s = node.getStart()) != null pr(s);
195 
196     // body or children
197 // escape needs to be only done in HtmlTranslator
198     // escape bodies of ASTPlain & ASTVerbatim (ASTUrl done in processURLs)
199     s = node.getBody();
200     if(s != null) {
201       if(node instanceof ASTPlain || node instanceof ASTVerbatim)
202         s = HtmlUtils.htmlEscape(s);
203       pr(s);
204     else {
205       for(int i = 0, j = node.jjtGetNumChildren(); i < j; i++) {
206         SimpleNode followerNode = null;
207         if((i+1< j
208           followerNode = (SimpleNodenode.jjtGetChild(i+1);
209         printTree((SimpleNodenode.jjtGetChild(i), inContents, followerNode);
210       }
211     }
212 
213     // end, endNotes, markup, after
214     if( (s = node.getEnd()) != null pr(s);
215     if(node instanceof ASTYamDocument) { printEndNotes()}
216     /*
217      * fix up nested sublists to be included in their parent list item:
218      * if is a listitem node
219      *   if the follower node is not null and is a list
220      *     add the node end to the end of the next node
221      *   else
222      *     output the node end
223      * else
224      *   output the node end
225      */
226     if(node instanceof ASTListItem) {
227       if(
228         nextNode != null && 
229         nextNode instanceof ASTUList || nextNode instanceof ASTOList )
230       ) {
231         if( (s = nextNode.getAfter()) == null s = "";
232         nextNode.setAfter(s + getConstantEnd(node.getId()));
233       else {
234         pr(getConstantEnd(node.getId()));
235       }
236     else {
237       pr(getConstantEnd(node.getId()));
238     }
239     if( (s = node.getAfter()) != null pr(s);
240   // printTree(SimpleNode, boolean, SimpleNode)
241 
242   /** Process URLs and Anchors. */
243   abstract public void processURLs(SimpleNode nodethrows GateException;
244 
245   /** Print end notes. */
246   public void printEndNotes() throws GateException {
247     if(footnotesList.size() != 0) {
248       prln(getConstantStart("footnoteSection"));
249       prln(makeHeader(null, 1"Footnotes", true, "cow-heading"));
250       prln(getConstantStart("olistPara"));
251 
252       for(int i=0; i<footnotesList.size(); i++) {
253         prln(getConstantStart("ListItem"));
254         String noteNumber = new Integer(i + 1).toString();
255         prln(getConstantStart("anchorPattern")
256           .replace("_Y_""cow-footnote-anchor")
257           .replace("_X_""footnote" + noteNumber));
258         pr(getConstantStart("footnoteText")
259           .replace("_X_""footnote" + noteNumber));
260         pr((StringfootnotesList.get(i));
261         prln(getConstantEnd("footnoteText"));
262         prln(getConstantEnd("ListItem"));
263       }
264       prln(getConstantEnd("olistPara"));
265       prln(getConstantEnd("footnoteSection"));
266     }
267     pr(footerStuff.toString());
268   // printEndNotes
269 
270   /** Material to insert at the end of the translation. */
271   public void addToFooter(String footerAddition) {
272     footerStuff.append(footerAddition);
273   // addToFooter(String)
274 
275   /** Material to insert at the end of the translation. */
276   StringBuilder footerStuff = new StringBuilder();
277 
278   /** Process a predicate node. */
279   void processPredicate(SimpleNode nodethrows GateException {
280     // start is the predicate name; args list has the args
281     String predName = node.getStart();
282     List argsList = node.getArgsList();
283     if(predName == nullthrow new GateException(
284       "predName is null: " ((SimpleNodenode).getImage()
285     );
286     if(argsList == nullthrow new GateException("argsList is null");
287     node.setStart(null);
288 
289     String[] predMarkup = (String[]) getConstantArray(predName);
290     if(predMarkup == null) { // unknown predicate or plugin
291       // is it a plugin?
292       Class pluginClass = null;
293       String predClassName =
294         predName.toUpperCase().charAt(0+ predName.substring(1);
295       try {
296         pluginClass =
297           Class.forName(YamFile.getPluginPackageName() "." + predClassName);
298       catch(ClassNotFoundException e) {
299       }
300 
301       if(pluginClass != null) { // plugin
302         Map pluginArgs = new HashMap();
303         for(int i=0; i<argsList.size(); i++) {
304           String[] keyValPair = ((StringargsList.get(i)).split("="2);
305           if(keyValPair.length != 2continue// we ignore invalid param specs
306           pluginArgs.put(keyValPair[0], keyValPair[1]);
307         }
308 
309         YamPlugin plugin = null;
310         try {
311           plugin = (YamPluginpluginClass.newInstance();
312         catch(InstantiationException e) {
313           return;
314         catch(IllegalAccessException e) {
315           return;
316         }
317 
318         pr(plugin.translate(pluginArgs, this, translatorType));
319         
320       else // unknown predicate
321         pr("%" + predName + "(");
322         for(int i=0; i<argsList.size(); i++)
323           pr((StringargsList.get(i" ");
324         pr(")");
325         parseTree.getWarnings().add(
326           new ParsingProblem(
327             node.getFirstToken().beginLine, node.getFirstToken().beginColumn,
328             node.getLastToken().endLine, node.getLastToken().endColumn, null,
329             "Unknown command %" + predName + " at line " +
330             node.getFirstToken().beginLine, false
331           )
332         );
333       }
334 
335       return;
336     // plugins and unknowns
337 
338     String markupStart = predMarkup[CONSTANTSTART];
339     String markupEnd = predMarkup[CONSTANTEND];
340     if(markupStart == nullthrow new GateException("markupStart is null");
341     if(markupEnd == nullthrow new GateException("markupEnd is null");
342 
343     // Citations: treated as a special case. To generalise, we would need to
344     // rework HtmlConstants and AbstractTranslator.
345     if(predName.equals("cite")) {
346       StringBuilder citeKeyBuilder = new StringBuilder();
347       StringBuilder linkBuilder = new StringBuilder();
348       for(int i=0; i<argsList.size(); i++) {
349         String key = (StringargsList.get(i);
350         if(i > 0) {
351           citeKeyBuilder.append(",");
352           linkBuilder.append(", ");
353         }
354         citeKeyBuilder.append(key);         
355         String link = getConstantStart("linkPattern");
356         try{
357           link = link.replace("_X_", ioHandler.getBibPageUrl().getURL()
358                               "#" + ioHandler.getBibAnchorPrefix() + key);
359         catch(IOException ioe){
360           log.warn("Couldn't resolve bibliography URL - citation links broken: "
361                     + ioHandler.getBibPageUrl());
362           link.replace("_X_", ioHandler.getBibAnchorPrefix() + key);
363         finally {
364           linkBuilder.append(link.replace("_Y_"(Stringkey));
365         }
366       }
367       pr(getConstantStart("cite").replace("_X_", citeKeyBuilder.toString()));
368       pr(linkBuilder.toString());
369       pr(getConstantEnd("cite"));
370       return;
371     }
372 
373     // add to footnote list, and create link here;
374     if(predName.equals("footnote")) {
375       if(argsList == null || argsList.size() == 0return;
376       String arg = (StringargsList.get(0);
377       if(arg == nullthrow new GateException("footnote body is null");
378       footnotesList.add(arg);
379       String noteNumber = new Integer(footnotesList.size()).toString();
380       pr(
381         "<span class=\"cow-footnote\" name=\"footnote" + noteNumber + "\"><sup>"
382         + getConstantStart("linkPattern")
383                  .replace("_X_""#footnote" + noteNumber)
384                  .replace("_Y_", noteNumber)
385         "</sup></span>"
386       );
387 
388       return;
389     }
390 
391     pr(markupStart);
392 
393     String[] attributes = (String[]) predicatesMap.get(predName);
394     for(int i=0; i<argsList.size(); i++) {
395       String arg = (StringargsList.get(i);
396       if(i >= attributes.length)
397         break;
398       else
399         pr(attributes[i].replace("_X_", arg));
400     }
401     pr(markupEnd);
402   // processPredicate(SimpleNode)
403 
404   /** Process a control node. */
405   void processControl(SimpleNode nodethrows GateException {
406     String type = node.getFirstToken().image;
407     Boolean active = new Boolean(true);
408 
409     if(controlsMap.get(type== null) {
410       pr(getConstantStart(type));
411       controlsMap.put(type, active);
412     else {
413       pr(getConstantEnd(type));
414       controlsMap.remove(type);
415     }
416   // processControl(SimpleNode)
417 
418   /**
419    * Collect contents and set heading bodies.
420    */
421   public void makeContents() throws GateException {
422     StringBuffer contents = new StringBuffer();
423     contents.append(makeHeader(null, 2"Contents", true, "cow-heading"));
424     contents.append("\n" + getConstantStart("ulistPara""\n");
425     List headings = parseTree.getHeadingNodes();
426     int MAXLEVELS = 10;
427     int contentsNumbering[] new int[MAXLEVELS];
428     for(int i=0; i<MAXLEVELS; i++)
429       contentsNumbering[i0;
430     int prevLevel = 0;
431 
432     // for each heading
433     for(int i=0; i<headings.size(); i++) {
434       SimpleNode node = (SimpleNodeheadings.get(i);
435       Token t = node.getFirstToken();
436       String number = t.image.trim();
437 
438       int level = 1;
439       boolean starred = false;
440       String lStr = "1";
441       String headingClass = "cow-heading";
442       if(number.startsWith("%"&& number.length() >= 2) {
443         level = Integer.parseInt(number.substring(12));
444         lStr = Integer.toString(level);
445         starred = number.endsWith("*");
446       else // section heads derived from titles
447         headingClass = "cow-title-heading";
448         level = Integer.parseInt(node.getBefore());
449         lStr = Integer.toString(level);
450         node.setBefore(null);
451       }
452 
453       // if the heading has a lower level than the previous non-starred
454       // heading, zero out the higher levels of contentsNumbering
455       if(!starred) {
456         if(level < prevLevel)
457           for(int l=(level+1); l<MAXLEVELS; l++)
458             contentsNumbering[l0;
459         prevLevel = level;
460       }
461 
462       if(level == 0) { level = 1; lStr = "1"// there's no level zero
463       String nodeImage = "";
464       String nodeContentsImage = "";
465       if(node.jjtGetNumChildren() 0) {
466         nodeImage = getNodeImage((SimpleNodenode.jjtGetChild(0)false);
467         nodeContentsImage =
468           getNodeImage((SimpleNodenode.jjtGetChild(0)true);
469       }
470 
471       // set up printing of the node from printTree
472       if(starred)
473         node.setBody(makeHeader(null, level, nodeImage, true, "cow-heading"));
474       else {
475         contentsNumbering[level]++;
476         String headNumber = getHeadingNumber(contentsNumbering, level);
477         String headNumberMarkup = getConstantStart("headNumber"+
478           headNumber + " " + getConstantEnd("headNumber");
479         contents.append(
480           getConstantStart("ListItem"+
481           getIndent(level+
482           getConstantStart("linkPattern")
483             .replace("_X_""#section-" + headNumber)
484             .replace("_Y_", headNumber + " " + nodeContentsImage+
485           getConstantEnd("ListItem""\n"
486         );
487         node.setBody(
488           getConstantStart("anchorPattern")
489             .replace("_X_""section-" + headNumber)
490             .replace("_Y_""cow-section-anchor"+
491           getConstantStart("headingPattern")
492             .replace("_X_", getConstantStart(lStr))
493             .replace("_Y_", headNumberMarkup + nodeImage)
494             .replace("_Z_", headingClass)
495         );
496       }
497     // for each heading
498     contents.append(getConstantEnd("ulistPara"));
499 
500     // set the contents node body
501     SimpleNode contentsNode = parseTree.getContentsNode();
502     if(contentsNode != nullcontentsNode.setBody(contents.toString());
503   // makeContents()
504 
505   /** Get indent spacing for a contents line. */
506   String getIndent(int level) {
507     StringBuffer indent = new StringBuffer();
508     for(int i=1; i<level; i++)
509       indent.append(getConstantStart("nbSpace"+ getConstantStart("nbSpace"));
510     return indent.toString();
511   // getIndent(int)
512 
513   /**
514    * Deal with the preamble material (and title if it exists).
515    */
516   public void makePreamble() throws GateException {
517     SimpleNode titleNode = parseTree.getTitleNode();
518 
519     if(titleNode == null) {
520       pr(makePreamble(""));
521     else {
522       String nodeImage = getNodeImage(titleNode);
523       pr(makePreamble(nodeImage));
524       pr(makeHeader(null, 1, nodeImage, true, "cow-title-heading"));
525     }
526   // makePreamble()
527 
528   /** Construct the preamble/header for the document. */
529   String makePreamble(String title) {
530     String header = null;
531     try {
532       header = Files.getResourceAsString(getPreamblePath());
533     catch(IOException e) {
534       // should never happen
535       throw new GateRuntimeException(
536         "couldn't find resource file " + getPreamblePath() ": " + e
537       );
538     }
539 
540     return header.replace("___TITLE___", title);
541   // makePreamble(String)
542 
543   /** Construct a header. */
544   String makeHeader (
545     int[] contentsNumbering, int level, String text, boolean markup,
546     String headingClass
547   throws GateException {
548     String number = getHeadingNumber(contentsNumbering, level);
549     if(!number.equals("")) number = number + " ";
550     String lStr = new Integer(level).toString();
551     String line = number + text;
552     if(markup)
553       try{
554       return getConstantStart("headingPattern")
555         .replace("_Z_", headingClass)
556         .replace("_X_", getConstantStart(lStr)).replace("_Y_", line);
557       catch(IllegalArgumentException iae) {
558         log.info("Failed heading: " + getConstantStart("headingPattern"));
559         log.info("Failed heading, X: " + lStr);
560         log.info("Failed heading, Y: " + line);
561         log.info("Failed heading, Z: " + headingClass);
562         throw new GateException("Failed to create heading: \"" + line
563                 "\"", iae);
564       }
565 
566     else
567       return line;
568   // makeHeader(int[], int, String, boolean, String)
569 
570   /** Derive a heading number string from a numbering array and the level. */
571   String getHeadingNumber(int[] contentsNumbering, int level) {
572     StringBuffer number = new StringBuffer();
573     if(contentsNumbering != null) {
574       for(int i=1; i<=level; i++)
575         number.append(Integer.toString(contentsNumbering[i]) ".");
576     }
577     return number.toString();
578   // getHeadingNumber(int[], int)
579 
580   /** Get a node's image from its tokens (using printTree). */
581   String getNodeImage(SimpleNode nodethrows GateException {
582     return getNodeImage(node, false);
583   // getNodeImage(SimpleNode)
584 
585   /** Get a node's image from its tokens (using printTree). */
586   String getNodeImage(SimpleNode node, boolean inContents)
587   throws GateException {
588     if(node == nullreturn "";
589     StringWriter swriter = new StringWriter();
590     Writer currentWriter = writer;
591     writer = swriter;
592     printTree(node, inContents);
593     writer = currentWriter;
594     return swriter.toString();
595   // getNodeImage(SimpleNode, boolean)
596 
597   /**
598    * Writer for translation results. Defaults to {@link gate.util.Out}'s
599    * Writer.
600    */
601   Writer writer = Out.getPrintWriter();
602 
603   /**
604    * Set the Writer for translation results.
605    */
606   public void setWriter(Writer writer) {
607     this.writer = writer;
608   }
609 
610   /** Debug flag. */
611   static final boolean debug = false;
612 
613   /** Write a string. */
614   void pr(String sthrows GateException {
615     try {
616       writer.write(s);
617       if(debug) { Out.pr(s); Out.flush()}
618     catch(IOException e) {
619       throw new GateException("can't write: " + e);
620     }
621   }
622 
623   /** Write a string plus a newline. */
624   void prln(String sthrows GateException {
625     try {
626       writer.write(s + "\n");
627       if(debug) { Out.prln(s); Out.flush()}
628     catch(IOException e) {
629       throw new GateException("can't write: " + e);
630     }
631   }
632 
633   /** Substitute newlines. */
634   String subNewlines(String s) {
635     return s.replaceAll("\n""\\\\n");
636   }
637 
638   /** Elide a string to fit a normal line. */
639   String elide(String line) {
640     StringBuffer elidedLine = new StringBuffer();
641     if(line.length() 78) {
642       elidedLine.append(line.substring(0,40));
643       elidedLine.append("...");
644       elidedLine.append(
645         line.substring(line.length() 30, line.length())
646       );
647     else 
648       return line;
649     return elidedLine.toString();
650   }
651 
652   /** The translator's IOHandler. */
653   IOHandler ioHandler;
654 
655   /** Set the translator's IOHandler.  */
656   public void setIOHandler(IOHandler ioh) { ioHandler = ioh; }
657 
658   /** Type of the translator - "Html", for example. */
659   public static String translatorType = "";
660 // AbstractTranslator