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
006package fc.util;
007
008import java.io.*;
009import java.util.*;
010import java.util.regex.*;
011
012import fc.io.*;
013
014/**
015Provides an ultra-simple template/merge type capability. Can be used
016standalone 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
052done by a special template variable, which is always called <tt>$code</tt> and is used with an
053argument that denotes the java object to call. For example: <tt>$code(mypackage.myclass)</tt> will
054call the <tt>code</tt> method in class <tt>mypackage.myclass</tt>. The specified custom class must
055implement the {@link fc.util.CustomCode} interface. The specified custom class must contain a
056no-arg constructor and is instantiated (once and only once) by the template engine. Global state
057can 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
060from the beginning of the template. If a corresponding value for a template variable is not found
061or a specified custom-code class cannot be loaded, that template variable is ignored (and removed)
062from the resulting output. So for example, if <tt>$foo</tt> does not have any value associated with
063it (i.e., is <tt>null</tt> by default), then it is simply removed in the resulting output.
064Similarly, if class <tt>mypkg.foo</tt> specified in the template text as <tt>$code(mypkg.foo)</tt>
065cannot 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).
068Templates are limited to simple variable substitution. However, the java code that
069creates/sets values for these variables can do any arbitrary looping, and using such
070loops, set the value of a template variable to any, arbitrarily large, string.
071</li>
072</ul>
073</blockquote>
074<p>
075Thread safety: This class is not thread safe and higher level synchronization should be used if
076shared by multiple threads.
077
078@author  hursh jain
079@date    3/28/2002
080**/
081public final class Template 
082{
083//class specific debugging messages: interest to implementors only
084private boolean dbg = false;
085//for the value() method - useful heuristic when writing to a charwriter
086private int approx_len;
087
088static final String dolsign = "{dolsign}";
089Map     datamap;
090List    template_actions;
091
092/* 
093pattern for checking the syntax of a template variable name. stops at next whitespace or at the end
094of the file, no need to specify any minimal searching. don't need dotall, or multiline for our
095purposes.
096
097group 1 gives the template variable or null
098group 2 gives the code class or null
099*/    
100Pattern namepat;
101
102/** 
103Constructs a new template object 
104@param  templatefile  the absolute path to the template file
105**/
106public 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/** 
117Constructs a new template object from the given String. Note, this is the the String to use as the
118template, <b>not</b> the name of a file. Various input streams can be converted into a template
119using methods from the {@link fc.io.IOUtil} class.
120**/
121public Template(String template) throws IOException
122  { 
123  Argcheck.notnull(template, getClass().getName() + ":<init> specified template parameter was null");
124  doInit(template);
125  }
126  
127private 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/** 
187Returns the template data, which is a <tt>Map</tt> of template variables to values (which were all
188set, using the {@link #set(String, String)} method. This map can be modified, as deemed necessary.
189**/
190public Map getTemplateData() {
191  return datamap;
192  }
193
194
195/** 
196Resets the template data. If reusing the template over and over again, this is faster, invoke this between
197each reuse. (rather than recreate it from scratch every time).
198**/
199public void reset() {
200  datamap.clear();
201  }
202
203
204/**
205A template variable can be assigned data using this method. This method should
206be 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**/
215public 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/**
224An alias for the {@link set} method.
225*/
226public void fill(String name, String value) {
227  set(name, value);
228  }
229
230/**
231An alias for the {@link set} method.
232*/
233public void fill(String name, int value) {
234  set(name, String.valueOf(value));
235  }
236
237/**
238An alias for the {@link set} method.
239*/
240public void fill(String name, long value) {
241  set(name, String.valueOf(value));
242  }
243
244/**
245An alias for the {@link set} method.
246*/
247public void fill(String name, boolean value) {
248  set(name, String.valueOf(value));
249  }
250
251
252/**
253An alias for the {@link set} method.
254*/
255public void fill(String name, Object value) {
256  set(name, String.valueOf(value));
257  }
258
259/** 
260Merges and writes the template and data. Always overwrites the specified
261destination (even if the destionation file already exists); 
262@param  destfile  the destination file (to write to).
263*/
264public void write(File destfile) throws IOException{
265  write(destfile, true);
266  }
267
268/** 
269Merges 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
271specified file already exists. 
272<p>
273The check to see whether an existing file is the same as the output file for this template is
274inherently system dependent. For example, on Windows, say an an existing file "foo.html" exist in
275the file system. Also suppose that the output file for this template is set to "FOO.html". This
276template then will then overwrite the data in the existing "foo.html" file but the output filename
277will not change to "FOO.html". This is because the windows filesystem treats both files as the same
278and 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*/
285public 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/** 
305Merges and writes the template and data to the specified Writer
306*/
307public void write(Writer out) throws IOException
308  {
309  mergeWrite(out);
310  }
311
312/** 
313Merges and writes the template and data to the specified Writer
314*/
315public void write(PrintStream out) throws IOException
316  {
317  mergeWrite(new PrintWriter(out));
318  }
319
320
321/** 
322Merges and writes the template and data and returns it as a String
323*/
324public String value() throws IOException
325  {
326  CharArrayWriter cw = new CharArrayWriter(approx_len);
327  mergeWrite(new PrintWriter(cw));
328  return cw.toString();
329  }
330
331/*
332don't call mergeWrite(out), if toString() invoked from a custom class or template variable, then it
333becomes recursive.
334*/
335public 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 -
362protected boolean checkNameSyntax(String name) {
363  return namepat.matcher(name).matches();
364  }
365
366
367protected 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
377abstract class TemplateAction 
378  {
379  public abstract void write(Writer writer) throws IOException;
380  }
381
382class 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
392class 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
402static HashMap loadedclasses = new HashMap(); 
403
404class 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/** 
436Unit Test History   
437<pre>
438Class Version Tester  Status    Notes
4391.0       hj    passed    
440</pre>
441**/
442public static void main(String[] args) throws Exception
443  {
444  new Test(args);   
445  }
446
447private static class Test
448{
449Test(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