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.pagetemplate;
007
008import java.io.*;
009import java.util.*;
010
011import fc.util.*;
012import fc.io.*;
013import fc.jdbc.*;
014
015/**
016A page template is a simplified molly page, that runs in a non web environment. Good for creating
017dynamic web pages (for email campaigns, etc) from the command line.
018<p>
019There is another alternate/simpler template mechanism here: {@link fc.util.Template}.
020<p>
021The page template loads template files (can have any suffix but .template is suggested as
022opposed to .mp to keep the non-web distinction clear). The template file is compiled
023(uses the system temp directory for compilation) as needed and the resulting class file
024is loaded/run. If a template is changed, it is automatically recompiled, reloaded and
025rerun. If the template has a compilation error, that page remains unloaded until the
026error is fixed.
027<p>
028The output of the template is written to an output destination (could be a buffer, string, file, etc)
029and the invoker can then send that output to an email destination.
030<p>
031Template pages only use the system classpath for compilation. Pages can refer to any class
032found in the system classpath. 
033<p>
034Here is some sample usage:
035<pre><code>
036TemplatePage pt = TemplateMgr.<b>getTemplate</b>("/path/to/file.template");
037pt.<b>render</b>(new File("/path/to/output.file"));
038
039//To optionally send information to the page, you can say (before calling render):
040Map m = new HashMap();
041m.put(...whatever..);
042pt.setContext(m);
043</code></pre>
044
045@author hursh jain
046*/
047public final class TemplateMgr
048{
049private static final boolean dbg = false;
050
051static Map  pagemap = new HashMap();
052static Log  log;
053static File tmpdir; 
054static String classpath;
055
056/**
057Returns a runnable page template, alias for the {@link getTemplate(String)} method.
058
059@param  templateFile  the full path to the template file
060*/
061public static TemplatePage get(String templateFilePath) throws Exception
062  {
063  return getTemplate(new File(templateFilePath)); 
064  }
065
066/**
067Returns a runnable page template, alias for the {@link getTemplate(File)} method.
068
069@param  templateFile  the full path to the template file
070*/
071public static TemplatePage get(File templateFile) throws Exception
072  {
073  return getTemplate(templateFile); 
074  }
075
076/**
077Returns a runnable page template.
078
079@param  templateFile  the full path to the template file
080*/
081public static TemplatePage getTemplate(String templateFilePath) throws Exception
082  {
083  return getTemplate(new File(templateFilePath)); 
084  }
085
086/**
087Returns a runnable page template.
088
089@param  templateFile  the template file
090*/
091public static TemplatePage getTemplate(File templateFile) throws Exception
092  {
093  if (! templateFile.exists())
094    throw new IllegalArgumentException("The specified template file " + templateFile + "] does not exist. How am I supposed to load it, eh ?");
095
096  StringBuilder buf = new StringBuilder(1028);
097
098  log = Log.getDefault();
099  classpath = System.getProperty("java.class.path");
100  tmpdir = new File(System.getProperty("java.io.tmpdir"));
101
102  TemplatePage page = null;
103
104  final CharArrayWriter fbuf = new CharArrayWriter(128);
105
106  String classname = getClassNameFromPageName(templateFile);
107  
108  File javafile   = new File(tmpdir, classname + ".java");
109  File classfile  = new File(tmpdir, classname + ".class");
110
111  if (dbg) {
112    System.out.println(
113      String.format("tmpdir=%s, TemplatePage=%s, javafile=%s, classfile=%s\n",
114          tmpdir, templateFile, javafile,classfile));
115    }
116
117  synchronized (TemplateMgr.class) 
118    {   
119    long page_modified  = templateFile.lastModified();
120    long java_modified  = javafile.lastModified();  //returns 0 if !exist
121    long class_modified = classfile.lastModified(); //returns 0 if !exist
122
123    if (dbg)
124      {
125      System.out.format(
126      " %-20s %10d\n %-20s %10d\n %-20s %10d\n", 
127          "Modified: page:", page_modified, 
128          "java:", java_modified, 
129          "class:", class_modified);
130      }
131    
132    if ( java_modified == 0L || page_modified > java_modified) 
133      {
134      if (dbg) System.out.format("page_mod > java_mod, parsing the page.........\n");
135      TemplateParser parser = new TemplateParser(templateFile, javafile, classname, log);
136
137      log.bug("PARSING page:", javafile.getPath());
138
139      try {
140        parser.parse();
141        }
142      catch (IOException e) {
143        //the parser may write a partially/badly written file if the parse failed.
144        if (javafile.exists()) {
145          javafile.delete();
146          }
147        throw e; //rethrow the parse exception
148        }
149        
150      java_modified   = javafile.lastModified();  //since newly parsed
151      }
152  
153    boolean forceReload = false;
154    //Java source could be generated or hacked by hand
155    // if nothing needs compiling, then we still need to load the
156    // page the first time it's accessed
157    if ( class_modified == 0L || java_modified > class_modified) 
158      {
159      if (dbg) System.out.format("java_mod > class_mod, compiling the java source.........\n");
160      
161      log.bug("COMPILING page:", javafile.getPath());
162      
163      //src_encoding can be null, that's fine.
164      TemplateCompiler pc = new TemplateCompiler(javafile, classpath);
165      
166      if (! pc.compile())
167        throw new TemplateParseException(pc.getError());
168      
169      forceReload = true; //since we recompiled, we reload even if
170                //page exists in the page cache
171      }
172
173    final boolean page_in_map = pagemap.containsKey(templateFile.getAbsolutePath());
174    
175    if (forceReload || ! page_in_map)
176      {
177      TemplateClassLoader loader = new TemplateClassLoader(log);
178      Class c = loader.loadClass(TemplatePage.PACKAGE_NAME + "." + classfile.getCanonicalPath());
179      
180      if (dbg) System.out.println("class = " + c);
181      page = (TemplatePage) c.newInstance();
182      if (dbg) System.out.println("page = " + page);
183      page.setSourcePath(templateFile); 
184          
185      if (page_in_map) {
186        TemplatePage oldpage = (TemplatePage) pagemap.get(templateFile.getAbsolutePath());
187        //notused
188        //oldpage.destroy();
189        }
190
191      pagemap.put(templateFile.getAbsolutePath(), page);
192      }
193    else{
194      page = (TemplatePage) pagemap.get(templateFile.getAbsolutePath());
195      }
196    }   
197  
198  if (dbg) log.bug("Returning PAGE=", page);
199  return page;
200  }
201
202/*
203a.template  ->  a_template
204b.template  ->  b_template
205c.template  ->  c_template
206p/q.template  ->  p_q.template
2075!#.template ->   5__template   ---> name clash 
2085!!.template ->   5__template   ---> name clash
209
210So a simplistic mapping(any bad classname chars--> '_') will not work.
211We should therefore hex-encode every special char..not sure if doing this right now.
212*/
213static String getClassNameFromPageName(File templateFile) throws IOException
214  {
215  if (dbg) System.out.println("getClassNameFromPageName(), page=["+templateFile+"]");
216    
217  StringBuilder buf = new StringBuilder();
218  char c;
219  
220  String path = templateFile.getAbsolutePath();
221  
222  c = path.charAt(0);
223  if (c == '/' || c == File.separatorChar) {
224    buf.append("_");
225    }
226  else if (! Character.isJavaIdentifierStart(c)) {
227    buf.append(Integer.toHexString(c));
228    }
229  else {
230    buf.append(c);
231    }
232  
233  for (int n = 1; n < path.length(); n++) 
234    {
235    c = path.charAt(n);
236    
237    if (c == '/' || c == File.separatorChar || c == '.') {
238      buf.append("_");
239      }
240    else {    
241      if (! Character.isJavaIdentifierPart(c))
242        buf.append(Integer.toHexString(c));
243      else 
244        buf.append(c);
245      }
246    }
247      
248  String str = buf.toString();
249
250  if (str.length() > 1 && str.startsWith("_")) {
251    str = str.substring(1);  //starting with _ is ugly
252    }
253    
254  return str;
255  }
256
257    
258/**
259Interactive page manager use for testing.
260*/
261public static void main (String args[]) throws Exception
262  {
263  Args myargs = new Args(args);
264  myargs.setUsage("java fc.util.template.TemplateMgr \n"
265    + "-file <path-to-page-template-file>\n"
266    + "-out <path-to-output-file>\n"
267        + " ------------------- optional ------------------ \n"
268      + "-conf <db-config file>\n"
269        + "-dbg|debug (turns log level to debug)\n"
270        );
271
272  log = Log.getDefault();
273  log.setLevel(Log.INFO);
274
275  if (myargs.flagExists("dbg") || myargs.flagExists("debug")) {
276      log.setLevel(Log.DEBUG);
277      }
278
279  Map m = new HashMap();
280  
281  String conf = myargs.get("conf");
282  if (conf != null) { 
283    PropertyMgr props = new FilePropertyMgr(new File(conf));
284    ConnectionMgr cmgr = new SimpleConnectionMgr(props);
285    m.put("cmgr", cmgr);
286    }
287  
288  TemplatePage pt = TemplateMgr.getTemplate(myargs.getRequired("file"));
289  pt.setContext(m);
290  pt.render(new File(myargs.getRequired("out")));
291  } //main
292
293}