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