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