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 final class Template 
082    {
083    //class specific debugging messages: interest to implementors only
084    private boolean dbg = false;
085    //for the value() method - useful heuristic when writing to a charwriter
086    private int approx_len;
087    
088    static final String dolsign = "{dolsign}";
089    Map     datamap;
090    List    template_actions;
091    
092    /* 
093    pattern for checking the syntax of a template variable name. stops at next whitespace or at the end
094    of the file, no need to specify any minimal searching. don't need dotall, or multiline for our
095    purposes.
096    
097    group 1 gives the template variable or null
098    group 2 gives the code class or null
099    */    
100    Pattern namepat;
101    
102    /** 
103    Constructs a new template object 
104    @param  templatefile  the absolute path to the template file
105    **/
106    public Template(File templatefile) throws IOException
107      {
108      Argcheck.notnull(templatefile, getClass().getName() + ":<init> specified templatefile parameter was null");
109      String templatepath = templatefile.getAbsolutePath();
110      String template = IOUtil.fileToString(templatepath);
111      if (template == null)
112        throw new IOException("The template file: " + templatepath + " could not be read");
113      doInit(template);
114      }
115      
116    /** 
117    Constructs a new template object from the given String. Note, this is the the String to use as the
118    template, <b>not</b> the name of a file. Various input streams can be converted into a template
119    using methods from the {@link fc.io.IOUtil} class.
120    **/
121    public Template(String template) throws IOException
122      { 
123      Argcheck.notnull(template, getClass().getName() + ":<init> specified template parameter was null");
124      doInit(template);
125      }
126      
127    private void doInit(String template)  throws IOException
128      {
129      /* the regex:
130        (             #g1 
131        \$(?!code)          #$ not followed by code
132        ([a-zA-Z](?:\w|-)*)     #g2: foo_name
133        )             #~g1
134      |
135        (             #g3
136        \$code\s*\          #$ followed by code and optional whitespace
137        (\s*([^\s]+)\s*\)     #g4: ( whitespace pkg.foo whitespace )
138        )
139      */
140      namepat = Pattern.compile(  
141        "(\\$(?!code)([a-zA-Z](?:\\w|-)*))|(\\$code\\s*\\(\\s*([^\\s]+)\\s*\\))" ,
142        Pattern.CASE_INSENSITIVE);  //for $code, $coDE etc.       
143      
144      datamap = new HashMap(); 
145      template_actions = new ArrayList(); 
146      
147      Matcher matcher = namepat.matcher(template); 
148      //find and save position of all template variables in textual order  
149      int pos = 0;
150      int len = template.length();
151    
152      while (matcher.find()) 
153        {
154        String g1 = matcher.group(1); String g2 = matcher.group(2);
155        String g3 = matcher.group(3); String g4 = matcher.group(4); 
156        if (dbg) System.out.println("found, begin:" + matcher.group() + ",g1=" + g1 + ", g2=" + g2 + ", g3=" + g3 + ", g4=" + g4);
157        if ( (g1 != null && g3 != null) || (g1 == null && g3 == null) )
158          throw new IOException("Error parsing template file, found input I don't understand:" + matcher.group());  
159        if (g1 != null) {  //$foo
160          int start = matcher.start(1);
161          if (dbg) System.out.println("g1:" + pos + "," + start);
162          template_actions.add(new Text(template.substring(pos,start)));
163          template_actions.add(new Var(g2));
164          pos = matcher.end(1);
165          if (dbg) System.out.println("finished g1");
166          }
167        else if (g3 != null) {  //$code(foo)
168          int start = matcher.start(3);
169          if (dbg) System.out.println("g3:" + pos + "," + start);
170          template_actions.add(new Text(template.substring(pos,start)));          
171          template_actions.add(new Code(g4));
172          pos = matcher.end(3);
173          if (dbg) System.out.println("finished g3");
174          }
175        } //~while
176      
177      if (pos != len) {
178        template_actions.add(new Text(template.substring(pos,len)));
179        }
180      
181      approx_len = template.length() * 2;
182    
183      if (dbg) System.out.println("template_actions = " + template_actions);
184      }   //~end constructor
185    
186    /** 
187    Returns the template data, which is a <tt>Map</tt> of template variables to values (which were all
188    set, using the {@link #set(String, String)} method. This map can be modified, as deemed necessary.
189    **/
190    public Map getTemplateData() {
191      return datamap;
192      }
193    
194    
195    /** 
196    Resets the template data. If reusing the template over and over again, this is faster, invoke this between
197    each reuse. (rather than recreate it from scratch every time).
198    **/
199    public void reset() {
200      datamap.clear();
201      }
202    
203    
204    /**
205    A template variable can be assigned data using this method. This method should
206    be called at least once for every unique template variable in the template.
207    
208    @param  name  the name of the template variable including the preceding<tt>$</tt>
209            sign. For example: <tt>$foo</tt> 
210    @param  value   the value to assign to the template variable.
211    
212    @throws IllegalArgumentException 
213        if the specified name of the template variable is not syntactically valid.              
214    **/
215    public void set(String name, String value) {
216      if (! checkNameSyntax(name)) {
217        throw new IllegalArgumentException("Template variable name " + name + " is not syntactically valid (when specifying variable names, *include* the \"$\" sign!)"); 
218        }
219      datamap.put(name, value);
220      }
221    
222    
223    /**
224    An alias for the {@link set} method.
225    */
226    public void fill(String name, String value) {
227      set(name, value);
228      }
229    
230    /**
231    An alias for the {@link set} method.
232    */
233    public void fill(String name, int value) {
234      set(name, String.valueOf(value));
235      }
236    
237    /**
238    An alias for the {@link set} method.
239    */
240    public void fill(String name, long value) {
241      set(name, String.valueOf(value));
242      }
243    
244    /**
245    An alias for the {@link set} method.
246    */
247    public void fill(String name, boolean value) {
248      set(name, String.valueOf(value));
249      }
250    
251    
252    /**
253    An alias for the {@link set} method.
254    */
255    public void fill(String name, Object value) {
256      set(name, String.valueOf(value));
257      }
258    
259    /** 
260    Merges and writes the template and data. Always overwrites the specified
261    destination (even if the destionation file already exists); 
262    @param  destfile  the destination file (to write to).
263    */
264    public void write(File destfile) throws IOException{
265      write(destfile, true);
266      }
267    
268    /** 
269    Merges and writes the template and data. Overwrites the specified destination, only if the
270    <tt>overwrite</tt> flag is specified as <tt>true</tt>, otherwise no output is written if the
271    specified file already exists. 
272    <p>
273    The check to see whether an existing file is the same as the output file for this template is
274    inherently system dependent. For example, on Windows, say an an existing file "foo.html" exist in
275    the file system. Also suppose that the output file for this template is set to "FOO.html". This
276    template then will then overwrite the data in the existing "foo.html" file but the output filename
277    will not change to "FOO.html". This is because the windows filesystem treats both files as the same
278    and a new File with a different case ("FOO.html") is <u>not</u> created.
279    
280    @param  destfile  the destination file (to write to).
281    @param  overwrite <tt>true</tt> to overwrite the destination
282    @throws IOException if an I/O error occurs or if the destination file cannot
283        be written to.
284    */
285    public void write(File destfile, boolean overwrite) throws IOException
286      {
287      Argcheck.notnull(destfile);
288      
289      if ( destfile.exists() ) 
290        {
291        if (! overwrite)  {
292          return;
293          }
294        if (! destfile.isFile()) {
295          throw new IOException("Specified file: " + destfile + " is not a regular file");
296          }
297        }
298        
299      BufferedWriter out = new BufferedWriter(new FileWriter(destfile));
300      mergeWrite(out);
301      }
302    
303    
304    /** 
305    Merges and writes the template and data to the specified Writer
306    */
307    public void write(Writer out) throws IOException
308      {
309      mergeWrite(out);
310      }
311    
312    /** 
313    Merges and writes the template and data to the specified Writer
314    */
315    public void write(PrintStream out) throws IOException
316      {
317      mergeWrite(new PrintWriter(out));
318      }
319    
320    
321    /** 
322    Merges and writes the template and data and returns it as a String
323    */
324    public String value() throws IOException
325      {
326      CharArrayWriter cw = new CharArrayWriter(approx_len);
327      mergeWrite(new PrintWriter(cw));
328      return cw.toString();
329      }
330    
331    /*
332    don't call mergeWrite(out), if toString() invoked from a custom class or template variable, then it
333    becomes recursive.
334    */
335    public String toString()
336      {
337      StringBuilder buf = new StringBuilder(approx_len);
338      for (int i = 0; i < template_actions.size(); i++)
339        {
340        TemplateAction act = (TemplateAction) template_actions.get(i);
341        if (act instanceof Text) {
342          buf.append(((Text)act).value);
343          }
344        else if (act instanceof Var) {
345          Object val = datamap.get("$" + ((Var)act).varname);
346          buf.append("[$" + ((Var)act).varname);
347          buf.append("]-->[");
348          buf.append(val);      
349          buf.append("]");      
350          }
351        else if (act instanceof Code) {
352          buf.append("Classname:[");
353          buf.append(((Code)act).classname);
354          buf.append("]");
355          }
356        }
357      return buf.toString();
358      }
359    
360    
361    //#mark -
362    protected boolean checkNameSyntax(String name) {
363      return namepat.matcher(name).matches();
364      }
365    
366    
367    protected void mergeWrite(Writer out) throws IOException
368      {
369      if (dbg) System.out.println("using datamap = " +  datamap);
370      Iterator it = template_actions.iterator();    
371      while (it.hasNext()) {
372        ((TemplateAction) it.next()).write(out);
373        }
374      out.close();
375      }
376    
377    abstract class TemplateAction 
378      {
379      public abstract void write(Writer writer) throws IOException;
380      }
381    
382    class Text extends TemplateAction
383      {
384      String value;
385      public Text(String val) { value = val; } 
386      public void write(Writer writer) throws IOException {
387        writer.write(value);
388        }
389      public String toString() { return "Text:" + value; }
390      }
391    
392    class Var extends TemplateAction {
393      String varname;
394      public Var(String name) { varname = name; }
395      public void write(Writer writer) throws IOException {
396        Object val = datamap.get("$" + varname);
397        writer.write( (val!=null) ? (String) val : "" );
398        }
399      public String toString() { return "Var:" + varname; }
400      }
401    
402    static HashMap loadedclasses = new HashMap(); 
403    
404    class Code extends TemplateAction  
405      {
406      String classname;
407      public Code(String classname) 
408        {
409        try {
410          if ( ! loadedclasses.containsKey(classname)) {
411            Class c = Class.forName(classname);
412            if (c != null) {
413              if (! CustomCode.class.isAssignableFrom(c))
414                return;
415              loadedclasses.put(classname, c.newInstance());
416              }
417            }
418          this.classname = classname;
419          }
420        catch (Exception e) {
421          e.printStackTrace();
422          }
423        }
424        
425      public void write(Writer writer) throws IOException {
426        Object obj = loadedclasses.get(classname);
427        if ( obj != null ) {
428          ((CustomCode)obj).code(writer, Template.this);
429          }
430        }
431      public String toString() { return "CustomClass: " + classname; }  
432      }
433    
434    //Template foo = this;  
435    /** 
436    Unit Test History   
437    <pre>
438    Class Version Tester  Status    Notes
439    1.0       hj    passed    
440    </pre>
441    **/
442    public static void main(String[] args) throws Exception
443      {
444      new Test(args);   
445      }
446    
447    private static class Test
448    {
449    Test(String[] args) throws Exception
450      {
451      String templateFileName = "test-template.txt";
452      String resultFileName = "template-merged.txt";
453    
454      System.out.println("Running test using template file: " + templateFileName);
455      File f = new File(templateFileName);
456      Template t = new Template(f);
457      t.set("$a", "a-value");
458      t.set("$b", "b-value");
459      t.set("$c", "c-value");
460      t.write(new File(resultFileName));
461      System.out.println("Completed. Results in: " + resultFileName);
462      }
463    } //~inner class test
464    
465    }           //~class Template