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