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 node) throws 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(singleLine) nl = "\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 = (SimpleNode) node.jjtGetChild(i+1);
209 printTree((SimpleNode) node.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 node) throws 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((String) footnotesList.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 node) throws 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 == null) throw new GateException(
284 "predName is null: " + ((SimpleNode) node).getImage()
285 );
286 if(argsList == null) throw 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 = ((String) argsList.get(i)).split("=", 2);
305 if(keyValPair.length != 2) continue; // we ignore invalid param specs
306 pluginArgs.put(keyValPair[0], keyValPair[1]);
307 }
308
309 YamPlugin plugin = null;
310 try {
311 plugin = (YamPlugin) pluginClass.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((String) argsList.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 == null) throw new GateException("markupStart is null");
341 if(markupEnd == null) throw 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 = (String) argsList.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_", (String) key));
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() == 0) return;
376 String arg = (String) argsList.get(0);
377 if(arg == null) throw 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 = (String) argsList.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 node) throws 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[i] = 0;
430 int prevLevel = 0;
431
432 // for each heading
433 for(int i=0; i<headings.size(); i++) {
434 SimpleNode node = (SimpleNode) headings.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(1, 2));
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[l] = 0;
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((SimpleNode) node.jjtGetChild(0), false);
467 nodeContentsImage =
468 getNodeImage((SimpleNode) node.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 != null) contentsNode.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 node) throws 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 == null) return "";
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 s) throws 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 s) throws 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
|