YamFile.java
001 /*
002  *  YamFile.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 
011 package gate.yam;
012 
013 import java.util.*;
014 import java.io.*;
015 import java.net.*;
016 import java.util.concurrent.*;
017 import org.apache.log4j.Logger;
018 import gnu.getopt.Getopt;
019 import org.springframework.core.io.*;
020 import org.springframework.context.support.*;
021 import org.springframework.util.StringUtils;
022 import gate.util.*;
023 import gate.yam.parse.*;
024 import gate.yam.translate.*;
025 
026 
027 /**
028  * Main interface to the YAM language and its translation.
029  * Target languages:
030  <ul>
031  <li>(X)HTML
032  <li>LaTeX
033  <li>PDF, which first translates to LaTeX then attempts to execute
034  <tt>pdflatex</tt>
035  <li>"TREE", which is a print representation of YAM parse trees used for
036  * testing
037  <li>"PRETTY", which is a pretty print of YAM (whitespace changes only)
038  </ul>
039  @author Hamish Cunningham
040  */
041 public class YamFile implements Serializable
042 {
043   /** Construction. */
044   YamFile(FileSystemResource location) { 
045     this.location = location;
046     outputTypes.add(FileType.HTML);
047   // YamFile(FileSystemResource)
048 
049   /** Construction. */
050   YamFile(File locationFile) { 
051     this.location = new FileSystemResource(locationFile);
052     outputTypes.add(FileType.HTML);
053   // YamFile(File)
054 
055   /** Logger */
056   static Logger log = Logger.getLogger("gate.yam.YamFile");
057 
058   /** Get the location of this file. */
059   public FileSystemResource getLocation() { return location; }
060 
061   /** Get the canonical path of this file. */
062   /**
063    * Giveas the canonical path of this YamFile
064    @return The canonical path of this YamFIle
065    @throws GateException If the canonical path of this YamFile causes an
066    * IOException
067    */
068   public String getCanonicalPath() throws GateException {
069     String path = null;
070 
071     try {
072       path = location.getFile().getCanonicalPath();
073     catch (IOException ioe) {
074       throw new GateException(ioe);
075     }
076 
077     return path;
078   // getCanonicalPath()
079 
080   /** Location of the .yam. */
081   transient FileSystemResource location;
082 
083   /** Allow serialization of the Spring FSR field. */
084   private void writeObject(ObjectOutputStream oosthrows IOException {
085     oos.defaultWriteObject();
086     oos.writeObject(location.getFile().getPath());
087   // writeObject(OOS)
088 
089   /** Allow serialization of the Spring FSR field. */
090   private void readObject(ObjectInputStream ois)
091   throws ClassNotFoundException, IOException {
092     ois.defaultReadObject();
093     location = new FileSystemResource((Stringois.readObject());
094   // readObject(OIS)
095 
096   /**
097    * Static factory method for getting a YAM file. Implemented via
098    {@link #get(FileSystemResource)}.
099    */
100   public static YamFile get(File location) {
101     YamFile yam = get(new FileSystemResource(location));
102     log.debug("location="+location+"; yam="+yam);
103     return yam;
104   // get(File)
105 
106   /**
107    * Static factory method for getting a YAM file. This method will create a
108    * YamFile for a particular location, or will
109    * return the YamFile dependency for a generated file (e.g. HTML). A null
110    * return value indicates that a dependent file was passed which is not
111    * generated from YAM.
112    @param location a path that points to a YAM file or a file depending on a
113    * YAM file.
114    @return either a YamFile instance or null.
115    */
116   public static YamFile get(FileSystemResource location) {
117     String path = location.getPath();
118     String lowerCasePath = path.toLowerCase();
119     log.debug("path = " + path);
120 
121     // is this a .yam? if so just return a new YamFile
122     if(lowerCasePath.endsWith(FileType.YAM.suffix())) {
123       return new YamFile(location);
124 
125     // is it a generated file? if so find and return the .yam
126     else if(FileType.dependent(path)) {
127       String base = path.substring(0, path.lastIndexOf('.'));
128       FileSystemResource yamLocation = 
129         new FileSystemResource(base + FileType.YAM.suffix());
130       File file = yamLocation.getFile();
131       if(file.exists()  &&  ! file.isDirectory())
132         return new YamFile(yamLocation);
133     }
134 
135     return null;
136   // get(FileSystemResource)
137 
138   /**
139    * Set the path in which to check for existence of relative links.
140    @param contextPath the path to search in.
141    */
142   public void setContextPath(String contextPath) {
143     this.ioHandler.setContextPath(contextPath);
144   // setContextPath(String)
145 
146   /**
147    * Reset the location of this YamFile in the directory pointed to by the
148    * context path. I.e. the new location of the file will be contextPath +
149    * new file name. This method is useful for when a generate has been
150    * done out of place (e.g. in the CoW staging directory) but now dependency
151    * analysis needs to be done on the file as if it was in place (e.g. when
152    * saving an edit and checking in).
153    @param name the file name of the 
154    */
155   public void replaceInContext(String name) {
156     File newLoc = new File(ioHandler.getContextPath(), name);
157     location = new FileSystemResource(newLoc);
158   // replaceInContext()
159 
160   /**
161    * Set the URL which non-existent links will be pointed at (which presumably
162    * is the "create new page" URL for the parent wiki).
163    @param createPageUrl the link to point to.
164    */
165   public void setCreatePageUrl(String createPageUrl) {
166     this.ioHandler.setCreatePageUrl(createPageUrl);
167   // setCreatePageUrl(String)
168 
169   /**
170    * Set the URL which non-existent links will be pointed at (which presumably
171    * is the "create new page" URL for the parent wiki).
172    @param createPageUrl the link to point to.
173    */
174   public void setCreatePageUrl(UrlResource createPageUrl) {
175     this.ioHandler.setCreatePageUrl(createPageUrl);
176   // setCreatePageUrl(UrlResource)
177 
178   /**
179    * Set the URL within which citation keys will be resolved. The URL will be
180    * for a bibliogrpahy. Citation keys reference a part of this URL, an
181    * individual bibliographic entry. Citation keys will form the final fragment
182    * of this URL, and will be prefixed, as defined by the
183    {@link #setBibAnchorPrefix(String) setBibAnchorPrefix} method
184    @param bibPageUrl the URL of the bibliogrpahy.
185    */
186   public void setBibPageUrl(UrlResource bibPageUrl) {
187     this.ioHandler.setBibPageUrl(bibPageUrl);
188   }
189 
190   /**
191    * Set the prefix added to citation keys, when forming a reference to a
192    * bibiography file entry
193    @see #setBibPageUrl(UrlResource)
194    @param bibAnchorPrefix The prefix added to citation keys.
195    */
196   public void setBibAnchorPrefix(String bibAnchorPrefix) {
197     this.ioHandler.setBibAnchorPrefix(bibAnchorPrefix);
198   }
199 
200   /**
201    * Create a file of a particular output type.
202    */
203   public File getOutputFile(FileType type) {
204     String path = location.getFile().getPath();
205     if(StringUtils.getFilename(path).contains("."))
206       path = path.substring(0, path.lastIndexOf('.'));
207 
208     return new File(path + type.suffix());
209   // getOutputFile(FileType)
210 
211   /** Get the File for HTML output. */
212   public File getHtmlFile() {
213     return getOutputFile(FileType.HTML);
214   // getHtmlFile()
215 
216   /** Get the path for an HTML output file. */
217   public String getHtmlPath() {
218     return getHtmlFile().getPath();
219   // getHtmlPath()
220 
221   /**
222    * Trigger generation of the various output types.
223    */
224   public void generateType(FileType type) {
225     outputTypes.add(type);
226   // generateType(FileType)
227 
228   /** What types of output to generate. */
229   Set<FileType> outputTypes = new HashSet<FileType>();
230 
231   /**
232    * The various file types involved with YAM and their filename suffixes.
233    */
234   public enum FileType {
235     YAM {
236       public String suffix() { return ".yam"}
237       public AbstractTranslator translator() { return null}
238     },
239     HTML {
240       public String suffix() { return ".html"}
241       public AbstractTranslator translator() { return new HtmlTranslator()}
242     },
243     LATEX {
244       public String suffix() { return ".tex"}
245       public AbstractTranslator translator() { return new LaTeXTranslator()}
246     },
247     PDF {
248       public String suffix() { return ".pdf"}
249       public AbstractTranslator translator() { return null/*new PDFTranslator();*/ }
250     },
251     TREE {
252       public String suffix() { return ".tree"}
253       public AbstractTranslator translator() { return new TreeTranslator()}
254     },
255     PRETTY {
256       public String suffix() { return ".pretty"}
257       public AbstractTranslator translator() { return new PrettyTranslator()}
258     };
259 
260     /** Get the file suffix for this type. */
261     public abstract String suffix();
262 
263     /** Get the translator class for this type. */
264     public abstract AbstractTranslator translator();
265 
266     /** Is this type generated from YAM files? */
267     public static boolean dependent(String path) {
268       path = path.toLowerCase();
269       return 
270         path.endsWith(HTML.suffix()) ||
271         path.endsWith(LATEX.suffix()) ||
272         path.endsWith(PDF.suffix())
273       ;
274     // dependent(path)
275   // FileType
276 
277   /**
278    * Get a List of those output file locations that are generated by
279    * this YamFile.
280    @throws GateException if there is an exception on the YamFile location.
281    @return a List of locations as Strings.
282    */
283   public List<String> getOutputLocations() throws GateException {
284     String id = null;
285     List<String> locations = new ArrayList<String>();
286 
287     id = getCanonicalPath();
288 
289     for(FileType type: outputTypes) {
290       // construct the output file name
291       StringBuilder outputPath = new StringBuilder(id);
292       int len = outputPath.length();
293       outputPath.replace(len - 4, len, type.suffix());
294       locations.add(outputPath.toString());
295     }
296 
297     return locations;
298   // getOutputLocations()
299 
300   /**
301    * Record of generation processes that are in progress. Maps from canonical
302    * path of the YAM file to the file types that are being processed.
303    */
304   static Map<String, Set<FileType>> currentGenerateCalls =
305     new ConcurrentHashMap<String, Set<FileType>>();
306      
307   /**
308    * This method checks whether a dependent file (one that is generated from
309    * YAM) needs regeneration, and if so it returns a YamFile instance
310    * corresponding to the generated file. If the file doesn't need
311    * regeneration or has not been generated from a YAM file the method returns
312    * null.
313    *
314    * A dependent file needs regeneration
315    * when
316    <ul>
317    <li>
318    * The YAM source file has an earlier modification time than the dependent
319    * file.
320    </ul>
321    *
322    @param location a file that may be generated from a YAM source file
323    @return either the YAM file that needs generation or null
324    @throws GateRuntimeException if we get an IO exception on the location
325    * file
326    */
327   public static YamFile needsGeneration(FileSystemResource location)
328   throws GateRuntimeException {
329     // is it dependent, and is there a corresponding .yam?
330     File dependentFile = location.getFile();
331     String path = null;
332     try {
333       path = dependentFile.getCanonicalPath();
334     catch(IOException e) {
335       throw new GateRuntimeException(e);
336     }
337     YamFile yam = null;
338     if(FileType.dependent(path)) yam = YamFile.get(location);
339     if(yam == nullreturn null;
340 
341     // is the .yam more recent than the dependent?
342     File dotYam = yam.getLocation().getFile();
343     if(dotYam.lastModified() > dependentFile.lastModified())
344       return yam;
345     else
346       return null;
347   // needsGeneration(location)
348 
349   /**
350    * The parse tree (and related state) created by {@link #generate()}.
351    */
352   transient YamParseTree parseTree = null;
353 
354   /**
355    * Get the parse tree (and related state) created by {@link #generate()}.
356    * If generate hasn't been run this will be null.
357    */
358   public YamParseTree getParseTree() { return parseTree; }
359 
360   /** Should we process included files or not? */
361   boolean doIncludes = true;
362 
363   /** Should we process included files or not? */
364   public void setDoIncludes(boolean b) { doIncludes = b; }
365 
366   /**
367    * Translate to target languages.
368    @throws GateException if parsing or translation fails.
369    @return the parse tree, or null if generation of any of our output 
370    * types is currently underway in a different YamFile.
371    */
372   public YamParseTree generate() throws GateException {
373     parseTree = null;
374     Reader reader = null;
375     String id = null;
376 
377     // this try block performs exclusion of our type(s) of generation on 
378     // this .yam; the finally clause releases the exclusion lock (from 
379     // the currentGenerateCalls map)
380     try {
381       id = location.getFile().getCanonicalPath();
382 
383       // record the type of generation against the canonical path and return
384       // null if a translation of the same type is already in progress
385       synchronized(currentGenerateCalls) {
386         Set currentCalls = currentGenerateCalls.get(id);
387         if(currentCalls == null) {
388           currentGenerateCalls.put(id, outputTypes);
389         else {
390           currentCalls.retainAll(outputTypes);
391           if(currentCalls.size() 0)
392             return null;
393         }
394       }
395 
396       // a reader for the .yam
397       reader = new FileReader(location.getFile());
398 
399       // parse
400       YamParser parser = new YamParser(reader);
401       ioHandler.setSourceDir(location.getFile().getParentFile());
402       parser.setIOHandler(ioHandler);
403       parser.setDoIncludes(doIncludes);
404       parseTree = parser.parse();
405 
406       // do the translation(s)
407       for(FileType type: outputTypes) {
408         // construct the output file name and get an appropriate writer
409         StringBuilder outputPath = new StringBuilder(id);
410         int len = outputPath.length();
411         outputPath.replace(len - 4, len, type.suffix());
412         Writer writer = new FileWriter(outputPath.toString());
413 
414         try {
415           // translate
416           translate(type, writer, parseTree);
417         }
418         finally {
419           writer.close();
420         }
421       }
422     
423     catch(IOException e) {
424       throw new GateException(e);
425     finally {
426       // remove this call from the currentGenerateCalls map
427       currentGenerateCalls.remove(id);
428 
429       // close the io stuff
430       try {
431         if(reader != nullreader.close();
432       catch(IOException ee) {
433         log.debug("problem closing IO in generate(), exception was: " + ee);
434       }
435     }
436 
437     return parseTree;
438   // generate()
439 
440   /**
441    * Get the list of links from this YAM file to other local files, as a list
442    * of canonical paths of those files. For files dependent on a YAM file, then
443    * the path of the YAM file will be returned. For files not dpendent on YAM
444    * files, the path of the file will be returned. Any links that cannot be
445    * resolved from their local path are ignored.
446    @return A list of the canonical paths of links in this YAM file. Null if
447    * the parse tree has not been generated via a call to generate(). Null if the
448    * parent directory of this YamFile cannot be resolved. Empty if
449    * there are no links.
450    */
451   public List<String> getLinks() {
452 
453     // return null if no parse tree has been generated
454     if(parseTree == null) {
455       log.info("yamFile.getLinks() failed to get links: no parse tree");
456       return null;
457     }
458 
459     // Return null if cannot get canonical path of parent directory
460     File base = null;
461     try {
462       base = location.getFile().getParentFile().getCanonicalFile();
463     catch(IOException ioe) {
464       log.info("yamFile.getLinks() failed to get links");
465       log.info("yamFile.getLinks() failed to find parent directory: "
466                + ioe.getMessage());
467     }
468 
469     // Return canonical paths of all links where we can get a canonical path
470     List<String> canonicalPaths = new ArrayList<String>();
471     for(String localPath : parseTree.getLinks()) {
472 
473       File absPath = new File(base, localPath);
474       YamFile yam = YamFile.get(new FileSystemResource(absPath));
475 
476       try {
477         if(yam == null) { // it's not yam or yam dependent
478           canonicalPaths.add(absPath.getCanonicalPath());
479         else // it's a yam or yam dependent
480           canonicalPaths.add(yam.getCanonicalPath());
481         }      
482       catch (IOException ioe) {
483         log.info("yamFile.getLinks() failed to resolve link to non-YAM file: "
484                 + ioe.getMessage());
485         log.info("yamFile.getLinks() ignored link: " + absPath);
486       catch (GateException ge) {
487         log.info("yamFile.getLinks() failed to resolve link to YAM file or "
488                 "YAM dependent file: "+ ge.getMessage());
489         log.info("yamFile.getLinks() ignored link: " + absPath);
490       }
491     }
492 
493     return canonicalPaths;
494   // getLinks()
495 
496   /**
497    * Get the list of includes from this YAM file to other local files, as a list
498    * of canonical paths of those files.  For files dependent on a YAM file, then
499    * the path of the YAM file will be returned. For files not dpendent on YAM
500    * files, the path of the file will be returned. Any includes that cannot be
501    * resolved from their local path are ignored.
502    @return A list of the canonical paths of includes in this YAM file. Null if
503    * the parse tree has not been generated via a call to generate(). Null if the
504    * parent directory of this YamFile cannot be resolved. Empty if
505    * there are no includes.
506    */
507   public List<String> getIncludes() {
508 
509     // return null if no parse tree has been generated
510     if(parseTree == null) {
511       log.info("yamFile.getIncludes() failed to get Includes: no parse tree");
512       return null;
513     }
514 
515     // Return null if cannot get canonical path of parent directory
516     File base = null;
517     try {
518       base = location.getFile().getParentFile().getCanonicalFile();
519     catch(IOException ioe) {
520       log.info("yamFile.getIncludes() failed to get includes");
521       log.info("yamFile.getIncludes() failed to find parent directory: "
522                + ioe.getMessage());
523     }
524 
525     // Return canonical paths of all Includes where we can get a canonical path
526     List<String> canonicalPaths = new ArrayList<String>();    
527     for(String localPath : parseTree.getIncludes()) {
528       File absPath = new File(base, localPath);
529       YamFile yam = YamFile.get(new FileSystemResource(absPath));
530 
531       try {
532         if(yam == null) {
533           // It's not yam or yam dependent. This shouldn't happen. Includes
534           // should only be of yams.
535           log.info("yamFile.getIncludes() found include of non-YAM "
536                 "file: " + absPath);
537           log.info("yamFile.getIncludes() ignored include: " + absPath);
538 
539         else {
540           // It's a yam or yam dependent
541           canonicalPaths.add(yam.getCanonicalPath());
542         }
543       catch (GateException ge) {
544         log.info("yamFile.getIncludes() failed to resolve include of YAM file: "
545                   + ge.getMessage());
546         log.info("yamFile.getIncludes() ignored include: " + absPath);
547       }
548 
549     }
550     return canonicalPaths;
551   // getIncludes()
552 
553 
554   /**
555    * Construct an appropriate translator for the target language and run it.
556    */
557   void translate(
558     FileType outputType, Writer outputWriter, YamParseTree parseTree
559   throws GateException {
560     AbstractTranslator translator = outputType.translator();
561     translator.setIOHandler(ioHandler);
562     translator.setParseTree(parseTree);
563     translator.setWriter(outputWriter);
564     translator.translate();
565   // translate(FileType, Writer, YamParseTree)
566 
567   /**
568    * Get the language version number.
569    * Version 1 was derived from Terrence Parr's TML language (thanks Ter!
570    * See <a href=http://antlr.org>the ANTLR site</a>).
571    * Version 2 was the first version of YAM proper. Version 3 added
572    * various new facilities and was the basis for the first version of CLIE.
573    * Version 4 was a complete rewrite, for GATE version 4 and for use in
574    * <a href=http://gatewiki.sf.net/>CoW</a>. Version 5 is intended to be
575    * stable and backwards-compatible with future versions.
576    */
577   public String getVersion() { return "5.0"}
578 
579   /** Get the package name for YAM plugins. */
580   public final static String getPluginPackageName() {
581     return "gate.yam.plugins";
582   // getPluginPackageName()
583 
584   /**
585    * Return a String representing this YamFile: the String representation of the
586    * underlying File.
587    @return A String representing this YamFile
588    */
589   public String toString() {
590     return location.toString();
591   }
592 
593   /** Print errors and warnings from parsing. */
594   public static String printErrors(YamParseTree parseTree) {
595     StringBuffer buf = new StringBuffer();
596     List errors = parseTree.getErrors();
597     List warnings = parseTree.getWarnings();
598     if(errors.size() == && warnings.size() == 0)
599       return buf.toString();
600     buf.append("%%%%%%%%%%%%%%% errors and warnings %%%%%%%%%%%%%%%%%%\n");
601     for(int i=0; i<errors.size(); i++)
602       buf.append(((ParsingProblem)errors.get(i)).getMessage() "\n");
603     for(int i=0; i<warnings.size(); i++)
604       buf.append(((ParsingProblem)warnings.get(i)).getMessage() "\n");
605     buf.append("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%");
606     return buf.toString();
607   // printErrors(parseTree)
608 
609   /** IOHandler to pass to the parser. */
610   IOHandler ioHandler = new IOHandlerImpl();
611 
612 // YamFile