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