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