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.web.page;
007
008import java.io.*;
009import java.util.*;
010
011import fc.util.*;
012import fc.io.*;
013
014/**
015Manages pages. Pages are found below a web document root directory. Pages
016are compiled as needed and the resulting class file is loaded/run. If a
017page is changed, it is automatically recompiled, reloaded and rerun. If the
018page has a compilation error, that page remains unloaded until the error is
019fixed.
020<p>
021A new PageMgr should be instantiated for each unique root directory (for
022example with multiple virtual hosts or users, each having their own root
023directory).
024
025@author hursh jain
026*/
027public final class PageMgr
028{
029private static final boolean dbg = false;
030
031File    docroot;
032File    scratchdir;    //for compiled pages.
033Map     pagemap = new HashMap();
034Log     log;
035String    classpath;
036PageServlet servlet;      //added this so a page can refer back to the parent servlet
037
038//used by performance hack in getPage(), remove if hack removed.
039String    docrootStr;
040String    scratchdirStr;    
041
042/**
043Constructs a new Page manager. The page manager will use the system
044classpath and <tt>/WEB-INF/classes</tt>, <tt>/WEB-INF/lib</tt> as the
045classpath for complilation. Pages can refer to any class found in those
046locations.
047
048@param  servlet   the Molly Servlet. This is optional and can be
049          <tt>null</tt> when creating/testing the PageMgr from
050          the command line.
051@param  docroot   absolute path to the document root directory within
052          which the pages are found. This is the directory that
053          correspond to the "/" location of the webapp.
054@param  scratchdir  absolute path to a scratch dirctory where intermediate
055          and temporary files can be written. This is where the
056          translated <tt>page-->java</tt> file will be created.
057@param  log     a logging destination.
058
059*/
060public PageMgr(PageServlet servlet, File docroot, File scratchdir, Log log) 
061  {
062  Argcheck.notnull(docroot, "docroot parameter was null");
063  
064  this.log = log;
065  this.servlet = servlet;
066  
067  if (! docroot.exists())
068    throw new IllegalArgumentException("The specified docroot" + docroot + "] does not exist. How am I supposed to load pages, eh ?");
069
070  if (! docroot.isDirectory())
071    throw new IllegalArgumentException("The specified docroot [" + docroot + "] is not a directory. Give me your webpage directory, fool !");
072
073  this.docroot = docroot; 
074  this.scratchdir = scratchdir;
075
076  this.docrootStr = docroot.getPath();  
077  this.scratchdirStr = scratchdir.getPath();
078
079  //we need to put /WEB-INF/classes, /WEB-INF/lib in the classpath too
080  StringBuilder buf = new StringBuilder(1028);
081  buf.append(System.getProperty("java.class.path"));
082  buf.append(File.pathSeparator);
083  File webinf =  new File(docroot, "WEB-INF");
084
085  buf.append(new File(webinf,"classes").getAbsolutePath());
086  File lib = new File(webinf, "lib");
087  if (lib.exists()) {
088    File[] list = lib.listFiles(new FilenameFilter() {
089      public boolean accept(File f, String name) {
090        return name.endsWith("zip") || name.endsWith("jar");
091        }
092      });
093    for (int n = 0; n < list.length; n++) {
094      buf.append(File.pathSeparator);
095      buf.append(list[n].getAbsolutePath());
096      }
097    }
098  classpath = buf.toString();
099
100  log.info("Created new PageMgr. Using: \n\t\tdocroot:     ", docroot, 
101      "\n\t\tscratchroot: ", scratchdir, "\n\t\tclasspath:   ", classpath);
102  }
103
104/*
105Internally called by pageservlet when it's unloaded. ensures that the
106destroy() method of every loaded page is called.
107*/
108void destroy()
109  {
110  Iterator i = pagemap.values().iterator();
111  while (i.hasNext()) {
112    Page p = (Page) i.next();
113    p.destroy();
114    }
115  }
116
117/**
118Returns the {@link Page} corresponding the the page path.
119
120@args contextRelativePagePath  path relative to the servlet context
121                 (e.g.: "/foo/bar/baz.mp"), the leading
122                 '/' is optional.
123*/
124public Page getPage(String contextRelativePath) throws Exception
125  {
126  Argcheck.notnull(contextRelativePath, "Internal error: the contextRelativePath parameter was null");
127  Page page = null;
128
129  final CharArrayWriter fbuf = new CharArrayWriter(128);
130
131  //File docroot_pagefile = new File(docroot, contextRelativePagePath);
132  //->micro-optimization
133  fbuf.append(docrootStr).append(File.separator).append(contextRelativePath); 
134  final File docroot_pagefile = new File(fbuf.toString());
135  fbuf.reset();
136  //->end mo
137
138  if (! docroot_pagefile.exists()) {
139    if (dbg) System.out.println("page: " + docroot_pagefile + " does not exist. Returning null page");
140    return null;
141    }
142
143  final String pagefile  = StringUtil.fileName(contextRelativePath);
144
145  /* this can be '/'  if page = /foo.mp or '' if page = foo.mp */
146  final String pagedir   = StringUtil.dirName(contextRelativePath);
147  
148  if (!  (pagedir.equals("/") || pagedir.equals("") )) {
149    //File pagedirFile = new File(scratchdir, pagedir);
150    //->micro-optimization
151    fbuf.append(scratchdirStr).append(File.separator).append(pagedir);
152    File pagedirFile = new File(fbuf.toString());
153    fbuf.reset();
154    //->end mo
155    
156    //we need to create this directory otherwise print/file writers
157    //that try to write a file within that directory will crap out.
158    if (! pagedirFile.exists()) {
159      if (dbg) System.out.println("Creating directory: " + pagedirFile);
160      pagedirFile.mkdirs();
161      }
162    }
163  
164  String classname = getClassNameFromPageName(pagedir, pagefile);
165  
166  /*
167  File javafile   = new File(scratchdir, 
168              pagedir + File.separator + classname + ".java");
169
170  File classfile  = new File(scratchdir, 
171              pagedir + File.separator + classname + ".class");
172  */
173  //->micro-optimization
174  fbuf.append(scratchdirStr).append(pagedir).append(File.separator)
175                  .append(classname).append(".java");
176  final File javafile = new File(fbuf.toString());
177  fbuf.reset();
178
179  fbuf.append(scratchdirStr).append(pagedir).append(File.separator)
180                  .append(classname).append(".class");
181  final File classfile = new File(fbuf.toString());
182  fbuf.reset();
183  //->end mo
184
185  if (dbg) {
186    System.out.println(
187      String.format("contextRelativePath=%s, pagedir=%s, pagefile=%s, javafile=%s, classfile=%s\n",
188          contextRelativePath, pagedir, pagefile, javafile, classfile));
189    }
190
191  synchronized (this) 
192    {   
193    String src_encoding = null;
194    long page_modified  = docroot_pagefile.lastModified();
195    long java_modified  = javafile.lastModified();  //returns 0 if !exist
196    long class_modified = classfile.lastModified(); //returns 0 if !exist
197
198    if (dbg)
199      {
200      System.out.format(
201      " %-20s %10d\n %-20s %10d\n %-20s %10d\n", 
202          "Modified: page:", page_modified, 
203          "java:", java_modified, 
204          "class:", class_modified);
205      }
206    
207    if ( java_modified == 0L || page_modified > java_modified) 
208      {
209      if (dbg) System.out.format("page_mod > java_mod, parsing the page.........\n");
210      PageParser parser = new PageParser(
211              docroot, docroot_pagefile, javafile, classname);
212
213      log.info("PARSING page:", javafile.getPath());
214
215      try {
216        parser.parse();
217        src_encoding = parser.getSourceEncoding();
218        if (src_encoding == null) {
219          src_encoding = Page.DEFAULT_SRC_ENCODING;
220          }
221        }
222      catch (IOException e) {
223        //the parser may write a partially/badly written file
224        //if the parse failed.
225        if (javafile.exists()) {
226          javafile.delete();
227          }
228        throw e; //rethrow the parse exception
229        }
230        
231      java_modified   = javafile.lastModified();  //since newly parsed
232      }
233  
234    boolean forceReload = false;
235    //Java source could be generated or hacked by hand
236    // if nothing needs compiling, then we still need to load the
237    // page the first time it's accessed
238    if ( class_modified == 0L || java_modified > class_modified) 
239      {
240      if (dbg) System.out.format("java_mod > class_mod, compiling the java source.........\n");
241      
242      log.info("COMPILING page:", javafile.getPath());
243      
244      //src_encoding can be null, that's fine.
245      PageCompiler pc = new PageCompiler(javafile, classpath, src_encoding);
246      
247      if (! pc.compile())
248        throw new ParseException(pc.getError());
249      
250      forceReload = true; //since we recompiled, we reload even if
251                //page exists in the page cache
252      }
253
254    final boolean page_in_map = pagemap.containsKey(contextRelativePath);
255    
256    if (forceReload || ! page_in_map)
257      {
258      PageClassLoader loader = new PageClassLoader(/*scratchdir*/);
259      Class c = loader.loadClass(
260        Page.PACKAGE_NAME + "." + classfile.getCanonicalPath());
261      
262      if (dbg) System.out.println("class = " + c);
263      page = (Page) c.newInstance();
264      if (dbg) System.out.println("page = " + page);
265      page.init(servlet, contextRelativePath);  
266      
267      //the pagemap uses contextRelativePath so that the
268      //we store /foo/bar.mp and /bar.mp differently.
269      //Also there should be a separate instance of 
270      //PageServlet per context/WEB-INF/web.xml, so
271      //different contexts will get different pageservlets
272      //and hence different pagemgr's (and the pagemap
273      //within each pagemgr will be different, hence
274      //same path names within each context won't stomp
275      //over each other).
276      
277      if (page_in_map) {
278        Page oldpage = (Page) pagemap.get(contextRelativePath);
279        oldpage.destroy();
280        }
281      //replace old page always
282      pagemap.put(contextRelativePath, page);
283      }
284    else{
285      page = (Page) pagemap.get(contextRelativePath);
286      }
287    }   
288  
289  if (dbg) log.bug("Returning PAGE=", page);
290  return page;
291  }
292
293/*
294a.mp  ->  a_mp
295b.mp  ->  b_mp
296c.mp  ->  c_mp
297p/q.mp  ->  p_q.mp
2985!#.mp ->   5__mp   ---> name clash 
2995!!.mp ->   5__mp   ---> name clash
300
301So a simplistic mapping(any bad classname chars--> '_') will not work.
302We therefore hex-encode every special char.
303*/
304static String getClassNameFromPageName(String dir, String page) throws IOException
305  {
306  if (dbg) System.out.println("getClassNameFromPageName(): dir=["+dir+"]; page=["+page+"]");
307    
308  StringBuilder buf = new StringBuilder();
309  char c;
310  
311  /*
312  url=/$/y.mp  dir=/$/ or possibly $/  --> name HH_y.mp  [HH=hex]
313  url=/y.mp  dir=/           --> name y.mp 
314  we don't want our names to always start with _ because that's hokey
315  */
316  if (dir.length() > 0)
317    {
318    boolean skip = false;
319    c = dir.charAt(0);
320    
321    if (c == '/' || c == File.separatorChar)
322      skip = true;      
323    
324    if (! skip)
325      {
326      if (! Character.isJavaIdentifierStart(c))
327        buf.append(Integer.toHexString(c));
328      else 
329        buf.append(c);
330      }
331      
332    if (dbg) System.out.println("buf3="+buf.toString());
333    
334    for (int n = 1; n < dir.length(); n++) 
335      {
336      c = dir.charAt(n);
337      
338      if (c == '/' || c == File.separatorChar)
339        c = '_';
340        
341      if (! Character.isJavaIdentifierPart(c))
342        buf.append(Integer.toHexString(c));
343      else 
344        buf.append(c);  
345      
346      if (dbg) System.out.println("buf4="+buf.toString());
347      }
348    }
349    
350  int dotpos = page.indexOf(".");
351  if (dotpos != -1)
352    page = page.substring(0,dotpos);
353  
354  c = page.charAt(0);
355
356  if (! Character.isJavaIdentifierPart(c))
357    buf.append(Integer.toHexString(c));
358  else 
359    buf.append(c);  
360  
361  for (int n = 1; n < page.length(); n++) 
362    {
363    c = page.charAt(n);
364    if (Character.isJavaIdentifierPart(c)) {
365      buf.append(c);
366      }
367    else{
368      if (dbg) System.out.println(">>>>>>> " + c + " -> " + Integer.toHexString(c));
369      buf.append(Integer.toHexString(c));
370      }
371    }
372  
373  buf.append("_mp");
374  return buf.toString();
375  }
376
377    
378/**
379Interactive page manager use/testing.
380*/
381public static void main (String args[]) throws Exception
382  {
383  Args myargs = new Args(args);
384  myargs.setUsage("java fc.web.page.PageMgr -docroot path-to-docroot-dir (. for cwd)] [-scratchroot path-to-scratchdir  (default .)]");
385  String docroot = myargs.getRequired("docroot");
386  String scratchroot = myargs.get("scratchroot", ".");
387  PageMgr pagemgr = new PageMgr(null,
388    new File(docroot), new File(scratchroot), Log.getDefault());
389  
390  //from a jdk techtip
391    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
392    String pagename = null;
393  while (true) 
394    {
395    try {
396      System.out.print("Enter LOAD <path-to-page>, RELOAD, GC, or QUIT: ");    
397      String cmdRead = br.readLine();
398      String[] toks = cmdRead.split("\\s+");
399      String cmd = toks[0].toUpperCase();
400      
401      if (cmd.equals("QUIT")) {
402        return;
403        } 
404      else if (cmd.equals("LOAD")) {
405        pagename = toks[1];
406        testLoad(pagemgr, pagename);
407        }   
408      else if (cmd.equals("RELOAD")) {
409        if (pagename == null)
410          System.out.println("Load a page first.....");
411        else
412          testLoad(pagemgr, pagename);
413        }   
414      else if (cmd.equals("GC")) {
415        System.gc();
416        System.runFinalization();
417        }
418      } //try
419    catch (Throwable e) {
420      e.printStackTrace();
421      }
422    } //while
423  } //main
424
425private static void testLoad(PageMgr pagemgr, String pagename) throws Exception
426  {
427  Page p = pagemgr.getPage(pagename);
428  if (p != null) 
429    System.out.println(ClassUtil.getClassLoaderInfo(p));
430  else
431    System.out.println("Could not load page. getPage() returned null");
432  }
433
434}