001    // Copyright (c) 2001 Hursh Jain (http://www.mollypages.org) 
002    // The Molly framework is freely distributable under the terms of an
003    // MIT-style license. For details, see the molly pages web site at:
004    // http://www.mollypages.org/. Use, modify, have fun !
005    
006    package fc.util;
007    
008    import java.io.*;
009    import java.util.*;
010    import java.util.regex.*;
011    
012    import fc.io.*;
013    
014    /**
015    Provides an ultra-simple template/merge type capability. Can be used
016    standalone or as part of a servlet/cgi environment.
017    <p> Template description:
018    <blockquote>
019    <ul>
020    <li>The template for the merge file can contain any data with embedded template variables.
021      <blockquote>
022      A template variable is a alphanumeric name and starts with a <tt>$</tt>. The name can also
023      contain the <tt>_</tt> and <tt>-</tt> characters. Examples of variable names are:
024      <i><tt>$foo</tt></i>, <i><tt>$bar9-abc</tt></i> and <i><tt>$ab_c</tt></i>. Names must begin
025      with a alphabet letter and not with a number or any other character. (this makes is easy to
026      have currency numbers like $123 in the template text, without it getting affected by the
027      template engine treating it as a variable). 
028      <p>
029      The <tt>$</tt> char itself can be output via a special template variable <b><font
030      color=blue><tt>${dolsign}</tt></font></b> which gets replaced by a literal <b><tt>$</tt></b> in
031      the output. Note, the braces are necessary in this case. So, to produce a literal <tt>$foo</tt>
032      in the output, there are two choices:
033        <blockquote>
034        <ul>
035          <li>set the value of $foo to the string "$foo", which will then be substituted in the
036          resulting output.
037          <li>specify ${dolsign}foo in the template text. The braces are necessary, because "$
038          foo", if found in the template text is simply ignored (not a valid variable) and hence
039          <tt>dolsign</tt> is not needed at all. For the relevant case <tt>$foo</tt>, the braces
040          serve to separate the word <tt>dolsign</tt> from <tt>foo</tt>.
041        </ul>
042        </blockquote>
043      <p>The template engine starts from the beginning of the template text and replaces each
044      template variable by it's corresponding value. This value is specified via {@link #set(String,
045      String)} method. Template variables are not recursively resolved, so if <tt>$foo</tt> has the value
046      "bar" and <tt>$bar</tt> has the value <tt>baz</tt>, then <tt>$$foo</tt> will be resolved to
047      <tt>$bar</tt> but the resulting <tt>$bar</tt> will <b>not</b> be resolved further (to
048      <tt>baz</tt>).
049      </blockquote>
050    </li>
051    <li>In addition to template variables, Templates can also contain custom java code. This can be
052    done by a special template variable, which is always called <tt>$code</tt> and is used with an
053    argument that denotes the java object to call. For example: <tt>$code(mypackage.myclass)</tt> will
054    call the <tt>code</tt> method in class <tt>mypackage.myclass</tt>. The specified custom class must
055    implement the {@link fc.util.CustomCode} interface. The specified custom class must contain a
056    no-arg constructor and is instantiated (once and only once) by the template engine. Global state
057    can be stored in a custom singleton object that can be created/used by the custom java code.
058    </li>
059    <li>The template engine executes in textual order, replacing/running code as it appears starting
060    from the beginning of the template. If a corresponding value for a template variable is not found
061    or a specified custom-code class cannot be loaded, that template variable is ignored (and removed)
062    from the resulting output. So for example, if <tt>$foo</tt> does not have any value associated with
063    it (i.e., is <tt>null</tt> by default), then it is simply removed in the resulting output.
064    Similarly, if class <tt>mypkg.foo</tt> specified in the template text as <tt>$code(mypkg.foo)</tt>
065    cannot be loaded/run, then it'll simply be ignored in the resulting output.
066    </li>
067    <li>Templates don't provide commands for looping (such as <tt>for</tt>, <tt>while</tt> etc).
068    Templates are limited to simple variable substitution. However, the java code that
069    creates/sets values for these variables can do any arbitrary looping, and using such
070    loops, set the value of a template variable to any, arbitrarily large, string.
071    </li>
072    </ul>
073    </blockquote>
074    <p>
075    Thread safety: This class is not thread safe and higher level synchronization should be used if
076    shared by multiple threads.
077    
078    @author  hursh jain
079    @date    3/28/2002
080    **/
081    public class Template 
082    {
083    //class specific debugging messages: interest to implementors only
084    private boolean dbg = false;
085    
086    static final String dolsign = "{dolsign}";
087    Map     datamap;
088    List    template_actions;
089    
090    /* 
091    pattern for checking the syntax of a template variable name. stops at next whitespace or at the end
092    of the file, no need to specify any minimal searching. don't need dotall, or multiline for our
093    purposes.
094    
095    group 1 gives the template variable or null
096    group 2 gives the code class or null
097    */    
098    Pattern namepat;
099    
100    /** 
101    Constructs a new template object 
102    @param  templatefile  the absolute path to the template file
103    **/
104    public Template(File templatefile) throws IOException
105      {
106      Argcheck.notnull(templatefile, getClass().getName() + ":<init> specified templatefile parameter was null");
107      String templatepath = templatefile.getAbsolutePath();
108      String template = IOUtil.fileToString(templatepath);
109      if (template == null)
110        throw new IOException("The template file: " + templatepath + " could not be read");
111      doInit(template);
112      }
113      
114    /** 
115    Constructs a new template object from the given String. Note, this is the the String to use as the
116    template, <b>not</b> the name of a file. Various input streams can be converted into a template
117    using methods from the {@link fc.io.IOUtil} class.
118    **/
119    public Template(String template) throws IOException
120      { 
121      Argcheck.notnull(template, getClass().getName() + ":<init> specified template parameter was null");
122      doInit(template);
123      }
124      
125    private void doInit(String template)  throws IOException
126      {
127      /* the regex:
128        (             #g1 
129        \$(?!code)          #$ not followed by code
130        ([a-zA-Z](?:\w|-)*)     #g2: foo_name
131        )             #~g1
132      |
133        (             #g3
134        \$code\s*\          #$ followed by code and optional whitespace
135        (\s*([^\s]+)\s*\)     #g4: ( whitespace pkg.foo whitespace )
136        )
137      */
138      namepat = Pattern.compile(  
139        "(\\$(?!code)([a-zA-Z](?:\\w|-)*))|(\\$code\\s*\\(\\s*([^\\s]+)\\s*\\))" ,
140        Pattern.CASE_INSENSITIVE);  //for $code, $coDE etc.       
141      
142      datamap = new HashMap(); 
143      template_actions = new ArrayList(); 
144      
145      Matcher matcher = namepat.matcher(template); 
146      //find and save position of all template variables in textual order  
147      int pos = 0;
148      int len = template.length();
149    
150      while (matcher.find()) 
151        {
152        String g1 = matcher.group(1); String g2 = matcher.group(2);
153        String g3 = matcher.group(3); String g4 = matcher.group(4); 
154        if (dbg) System.out.println("found, begin:" + matcher.group() + ",g1=" + g1 + " ,g2=" + g2 + " ,g3=" + g3 + " ,g4=" + g4);
155        if ( (g1 != null && g3 != null) || (g1 == null && g3 == null) )
156          throw new IOException("Error parsing template file, found input I don't understand:" + matcher.group());  
157        if (g1 != null) {  //$foo
158          int start = matcher.start(1);
159          if (dbg) System.out.println("g1:" + pos + "," + start);
160          template_actions.add(new Text(template.substring(pos,start)));
161          template_actions.add(new Var(g2));
162          pos = matcher.end(1);
163          if (dbg) System.out.println("finished g1");
164          }
165        else if (g3 != null) {  //$code(foo)
166          int start = matcher.start(3);
167          if (dbg) System.out.println("g3:" + pos + "," + start);
168          template_actions.add(new Text(template.substring(pos,start)));          
169          template_actions.add(new Code(g4));
170          pos = matcher.end(3);
171          if (dbg) System.out.println("finished g3");
172          }
173        } //~while
174      
175      if (pos != len)
176        template_actions.add(new Text(template.substring(pos,len)));
177      
178      if (dbg) System.out.println("template_actions = " + template_actions);
179      }   //~end constructor
180    
181    /** 
182    Returns the template data, which is a <tt>Map</tt> of template variables to values (which were all
183    set, using the {@link #set(String, String)} method. This map can be modified, as deemed necessary.
184    **/
185    public Map getTemplateData() {
186      return datamap;
187      }
188    
189    
190    /** 
191    Resets the template data. If reusing the template over and over again, this is faster, invoke this between
192    each reuse. (rather than recreate it from scratch every time).
193    **/
194    public void reset() {
195      datamap.clear();
196      }
197    
198    
199    /**
200    A template variable can be assigned data using this method. This method should
201    be called at least once for every unique template variable in the template.
202    
203    @param  name  the name of the template variable including the preceding<tt>$</tt>
204            sign. For example: <tt>$foo</tt> 
205    @param  value   the value to assign to the template variable.
206    
207    @throws IllegalArgumentException 
208        if the specified name of the template variable is not syntactically valid.              
209    **/
210    public void set(String name, String value) {
211      if (! checkNameSyntax(name))
212        throw new IllegalArgumentException("Template variable name " + name + " is not syntactically valid"); 
213      datamap.put(name, value);
214      }
215    
216    
217    /**
218    An alias for the {@link set} method.
219    */
220    public void fill(String name, String value) {
221      set(name, value);
222      }
223    
224    /**
225    An alias for the {@link set} method.
226    */
227    public void fill(String name, int value) {
228      set(name, String.valueOf(value));
229      }
230    
231    /**
232    An alias for the {@link set} method.
233    */
234    public void fill(String name, long value) {
235      set(name, String.valueOf(value));
236      }
237    
238    /**
239    An alias for the {@link set} method.
240    */
241    public void fill(String name, boolean value) {
242      set(name, String.valueOf(value));
243      }
244    
245    
246    /**
247    An alias for the {@link set} method.
248    */
249    public void fill(String name, Object value) {
250      set(name, String.valueOf(value));
251      }
252    
253    /** 
254    Merges and writes the template and data. Always overwrites the specified
255    destination (even if the destionation file already exists); 
256    @param  destfile  the destination file (to write to).
257    */
258    public void write(File destfile) throws IOException{
259      write(destfile, true);
260      }
261    
262    /** 
263    Merges and writes the template and data. Overwrites the specified destination, only if the
264    <tt>overwrite</tt> flag is specified as <tt>true</tt>, otherwise no output is written if the
265    specified file already exists. 
266    <p>
267    The check to see whether an existing file is the same as the output file for this template is
268    inherently system dependent. For example, on Windows, say an an existing file "foo.html" exist in
269    the file system. Also suppose that the output file for this template is set to "FOO.html". This
270    template then will then overwrite the data in the existing "foo.html" file but the output filename
271    will not change to "FOO.html". This is because the windows filesystem treats both files as the same
272    and a new File with a different case ("FOO.html") is <u>not</u> created.
273    
274    @param  destfile  the destination file (to write to).
275    @param  overwrite <tt>true</tt> to overwrite the destination
276    @throws IOException if an I/O error occurs or if the destination file cannot
277        be written to.
278    */
279    public void write(File destfile, boolean overwrite) throws IOException
280      {
281      Argcheck.notnull(destfile);
282      
283      if ( destfile.exists() ) 
284        {
285        if (! overwrite)  {
286          return;
287          }
288        if (! destfile.isFile()) {
289          throw new IOException("Specified file: " + destfile + " is not a regular file");
290          }
291        }
292        
293      BufferedWriter out = new BufferedWriter(new FileWriter(destfile));
294      mergeWrite(out);
295      }
296    
297    
298    /** 
299    Merges and writes the template and data to the specified Writer
300    */
301    public void write(Writer out) throws IOException
302      {
303      mergeWrite(out);
304      }
305    
306    /** 
307    Merges and writes the template and data to the specified Writer
308    */
309    public void write(PrintStream out) throws IOException
310      {
311      mergeWrite(new PrintWriter(out));
312      }
313    
314    
315    /*
316    don't call mergeWrite(out), if toString() invoked from a custom class or template variable, then it
317    becomes recursive.
318    */
319    public String toString()
320      {
321      return "Template, data=" + datamap;
322      }
323    
324    
325    //#mark -
326    protected boolean checkNameSyntax(String name) {
327      return namepat.matcher(name).matches();
328      }
329    
330    
331    protected void mergeWrite(Writer out) throws IOException
332      {
333      if (dbg) System.out.println("using datamap = " +  datamap);
334      Iterator it = template_actions.iterator();    
335      while (it.hasNext()) {
336        ((TemplateAction) it.next()).write(out);
337        }
338      out.close();
339      }
340    
341    abstract class TemplateAction 
342      {
343      public abstract void write(Writer writer) throws IOException;
344      }
345    
346    class Text extends TemplateAction
347      {
348      String value;
349      public Text(String val) { value = val; } 
350      public void write(Writer writer) throws IOException {
351        writer.write(value);
352        }
353      public String toString() { return "Text:" + value; }
354      }
355    
356    class Var extends TemplateAction {
357      String varname;
358      public Var(String name) { varname = name; }
359      public void write(Writer writer) throws IOException {
360        Object val = datamap.get("$" + varname);
361        writer.write( (val!=null) ? (String) val : "" );
362        }
363      public String toString() { return "Var:" + varname; }
364      }
365    
366    static HashMap loadedclasses = new HashMap(); 
367    
368    class Code extends TemplateAction  
369      {
370      String classname;
371      public Code(String classname) 
372        {
373        try {
374          if ( ! loadedclasses.containsKey(classname)) {
375            Class c = Class.forName(classname);
376            if (c != null) {
377              if (! CustomCode.class.isAssignableFrom(c))
378                return;
379              loadedclasses.put(classname, c.newInstance());
380              }
381            }
382          this.classname = classname;
383          }
384        catch (Exception e) {
385          e.printStackTrace();
386          }
387        }
388        
389      public void write(Writer writer) throws IOException {
390        Object obj = loadedclasses.get(classname);
391        if ( obj != null ) {
392          ((CustomCode)obj).code(writer, Template.this);
393          }
394        }
395      public String toString() { return "CustomClass: " + classname; }  
396      }
397    
398    //Template foo = this;  
399    /** 
400    Unit Test History   
401    <pre>
402    Class Version Tester  Status    Notes
403    1.0       hj    passed    
404    </pre>
405    **/
406    public static void main(String[] args) throws Exception
407      {
408      new Test(args);   
409      }
410    
411    private static class Test
412    {
413    Test(String[] args) throws Exception
414      {
415      String templateFileName = "test-template.txt";
416      String resultFileName = "template-merged.txt";
417    
418      System.out.println("Running test using template file: " + templateFileName);
419      File f = new File(templateFileName);
420      Template t = new Template(f);
421      t.set("$a", "a-value");
422      t.set("$b", "b-value");
423      t.set("$c", "c-value");
424      t.write(new File(resultFileName));
425      System.out.println("Completed. Results in: " + resultFileName);
426      }
427    } //~inner class test
428    
429    }           //~class Template