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 <code>/WEB-INF/classes</code>, <code>/WEB-INF/lib</code> 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          <code>null</code> 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 <code>page-->java</code> 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\t\tdocroot:     ", docroot, 
101      "\n\t\t\tscratchroot: ", scratchdir, 
102      "\n\t\t\tclasspath:   ", classpath);
103  }
104
105/*
106Internally called by pageservlet when it's unloaded. ensures that the
107destroy() method of every loaded page is called.
108*/
109void destroy()
110  {
111  Iterator i = pagemap.values().iterator();
112  while (i.hasNext()) {
113    Page p = (Page) i.next();
114    p.destroy();
115    }
116  }
117
118/**
119Returns the {@link Page} corresponding the the page path.
120
121@param  contextRelativePath  path relative to the servlet context
122                 (e.g.: "/foo/bar/baz.mp"), the leading
123                 '/' is optional.
124*/
125public Page getPage(String contextRelativePath) throws Exception
126  {
127  Argcheck.notnull(contextRelativePath, "Internal error: the contextRelativePath parameter was null");
128  Page page = null;
129
130  final CharArrayWriter fbuf = new CharArrayWriter(128);
131
132  //File docroot_pagefile = new File(docroot, contextRelativePagePath);
133  //->micro-optimization
134  fbuf.append(docrootStr).append(File.separator).append(contextRelativePath); 
135  final File docroot_pagefile = new File(fbuf.toString());
136  fbuf.reset();
137  //->end mo
138
139  if (! docroot_pagefile.exists()) {
140    if (dbg) System.out.println("page: " + docroot_pagefile + " does not exist. Returning null page");
141    return null;
142    }
143
144  final String pagefile  = StringUtil.fileName(contextRelativePath);
145
146  /* this can be '/'  if page = /foo.mp or '' if page = foo.mp */
147  final String pagedir   = StringUtil.dirName(contextRelativePath);
148  
149  if (!  (pagedir.equals("/") || pagedir.equals("") )) {
150    //File pagedirFile = new File(scratchdir, pagedir);
151    //->micro-optimization
152    fbuf.append(scratchdirStr).append(File.separator).append(pagedir);
153    File pagedirFile = new File(fbuf.toString());
154    fbuf.reset();
155    //->end mo
156    
157    //we need to create this directory otherwise print/file writers
158    //that try to write a file within that directory will crap out.
159    if (! pagedirFile.exists()) {
160      if (dbg) System.out.println("Creating directory: " + pagedirFile);
161      pagedirFile.mkdirs();
162      }
163    }
164  
165  String classname = getClassNameFromPageName(pagedir, pagefile);
166  
167  /*
168  File javafile   = new File(scratchdir, 
169              pagedir + File.separator + classname + ".java");
170
171  File classfile  = new File(scratchdir, 
172              pagedir + File.separator + classname + ".class");
173  */
174  //->micro-optimization
175  fbuf.append(scratchdirStr).append(pagedir).append(File.separator)
176                  .append(classname).append(".java");
177  final File javafile = new File(fbuf.toString());
178  fbuf.reset();
179
180  fbuf.append(scratchdirStr).append(pagedir).append(File.separator)
181                  .append(classname).append(".class");
182  final File classfile = new File(fbuf.toString());
183  fbuf.reset();
184  //->end mo
185
186  if (dbg) {
187    System.out.println(
188      String.format("contextRelativePath=%s, pagedir=%s, pagefile=%s, javafile=%s, classfile=%s\n",
189          contextRelativePath, pagedir, pagefile, javafile, classfile));
190    }
191
192  synchronized (this) 
193    {   
194    String src_encoding = null;
195    long page_modified  = docroot_pagefile.lastModified();
196    long java_modified  = javafile.lastModified();  //returns 0 if !exist
197    long class_modified = classfile.lastModified(); //returns 0 if !exist
198
199    if (dbg)
200      {
201      System.out.format(
202      " %-20s %10d\n %-20s %10d\n %-20s %10d\n", 
203          "Modified: page:", page_modified, 
204          "java:", java_modified, 
205          "class:", class_modified);
206      }
207    
208    if ( java_modified == 0L || page_modified > java_modified) 
209      {
210      if (dbg) System.out.format("page_mod > java_mod, parsing the page.........\n");
211      PageParser parser = new PageParser(
212              docroot, docroot_pagefile, javafile, classname);
213
214      log.info("PARSING page:", javafile.getPath());
215
216      try {
217        parser.parse();
218        src_encoding = parser.getSourceEncoding();
219        if (src_encoding == null) {
220          src_encoding = Page.DEFAULT_SRC_ENCODING;
221          }
222        }
223      catch (IOException e) {
224        //the parser may write a partially/badly written file
225        //if the parse failed.
226        if (javafile.exists()) {
227          javafile.delete();
228          }
229        throw e; //rethrow the parse exception
230        }
231        
232      java_modified   = javafile.lastModified();  //since newly parsed
233      }
234  
235    boolean forceReload = false;
236    //Java source could be generated or hacked by hand
237    // if nothing needs compiling, then we still need to load the
238    // page the first time it's accessed
239    if ( class_modified == 0L || java_modified > class_modified) 
240      {
241      if (dbg) System.out.format("java_mod > class_mod, compiling the java source.........\n");
242      
243      log.info("COMPILING page:", javafile.getPath());
244      
245      //src_encoding can be null, that's fine.
246      PageCompiler pc = new PageCompiler(javafile, classpath, src_encoding);
247      
248      if (! pc.compile())
249        throw new ParseException(pc.getError());
250      
251      forceReload = true; //since we recompiled, we reload even if
252                //page exists in the page cache
253      }
254
255    final boolean page_in_map = pagemap.containsKey(contextRelativePath);
256    
257    if (forceReload || ! page_in_map)
258      {
259      PageClassLoader loader = new PageClassLoader(/*scratchdir*/);
260      Class c = loader.loadClass(
261        Page.PACKAGE_NAME + "." + classfile.getCanonicalPath());
262      
263      if (dbg) System.out.println("class = " + c);
264      page = (Page) c.newInstance();
265      if (dbg) System.out.println("page = " + page);
266      page.init(servlet, contextRelativePath);  
267      
268      //the pagemap uses contextRelativePath so that the
269      //we store /foo/bar.mp and /bar.mp differently.
270      //Also there should be a separate instance of 
271      //PageServlet per context/WEB-INF/web.xml, so
272      //different contexts will get different pageservlets
273      //and hence different pagemgr's (and the pagemap
274      //within each pagemgr will be different, hence
275      //same path names within each context won't stomp
276      //over each other).
277      
278      if (page_in_map) {
279        Page oldpage = (Page) pagemap.get(contextRelativePath);
280        oldpage.destroy();
281        }
282      //replace old page always
283      pagemap.put(contextRelativePath, page);
284      }
285    else{
286      page = (Page) pagemap.get(contextRelativePath);
287      }
288    }   
289  
290  if (dbg) log.bug("Returning PAGE=", page);
291  return page;
292  }
293
294/*
295a.mp  ->  a_mp
296b.mp  ->  b_mp
297c.mp  ->  c_mp
298p/q.mp  ->  p_q.mp
2995!#.mp ->   5__mp   ---> name clash 
3005!!.mp ->   5__mp   ---> name clash
301
302So a simplistic mapping(any bad classname chars--> '_') will not work.
303We therefore hex-encode every special char.
304*/
305static String getClassNameFromPageName(String dir, String page) throws IOException
306  {
307  if (dbg) System.out.println("getClassNameFromPageName(): dir=["+dir+"]; page=["+page+"]");
308    
309  StringBuilder buf = new StringBuilder();
310  char c;
311  
312  /*
313  url=/$/y.mp  dir=/$/ or possibly $/  --> name HH_y.mp  [HH=hex]
314  url=/y.mp  dir=/           --> name y.mp 
315  we don't want our names to always start with _ because that's hokey
316  */
317  if (dir.length() > 0)
318    {
319    boolean skip = false;
320    c = dir.charAt(0);
321    
322    if (c == '/' || c == File.separatorChar)
323      skip = true;      
324    
325    if (! skip)
326      {
327      if (! Character.isJavaIdentifierStart(c))
328        buf.append(Integer.toHexString(c));
329      else 
330        buf.append(c);
331      }
332      
333    if (dbg) System.out.println("buf3="+buf.toString());
334    
335    for (int n = 1; n < dir.length(); n++) 
336      {
337      c = dir.charAt(n);
338      
339      if (c == '/' || c == File.separatorChar)
340        c = '_';
341        
342      if (! Character.isJavaIdentifierPart(c))
343        buf.append(Integer.toHexString(c));
344      else 
345        buf.append(c);  
346      
347      if (dbg) System.out.println("buf4="+buf.toString());
348      }
349    }
350    
351  int dotpos = page.indexOf(".");
352  if (dotpos != -1)
353    page = page.substring(0,dotpos);
354  
355  c = page.charAt(0);
356
357  if (! Character.isJavaIdentifierPart(c))
358    buf.append(Integer.toHexString(c));
359  else 
360    buf.append(c);  
361  
362  for (int n = 1; n < page.length(); n++) 
363    {
364    c = page.charAt(n);
365    if (Character.isJavaIdentifierPart(c)) {
366      buf.append(c);
367      }
368    else{
369      if (dbg) System.out.println(">>>>>>> " + c + " -> " + Integer.toHexString(c));
370      buf.append(Integer.toHexString(c));
371      }
372    }
373  
374  buf.append("_mp");
375  return buf.toString();
376  }
377
378    
379/**
380Interactive page manager use/testing.
381*/
382public static void main (String args[]) throws Exception
383  {
384  Args myargs = new Args(args);
385  myargs.setUsage("java fc.web.page.PageMgr -docroot path-to-docroot-dir (. for cwd)] [-scratchroot path-to-scratchdir  (default .)]");
386  String docroot = myargs.getRequired("docroot");
387  String scratchroot = myargs.get("scratchroot", ".");
388  PageMgr pagemgr = new PageMgr(null,
389    new File(docroot), new File(scratchroot), Log.getDefault());
390  
391  //from a jdk techtip
392    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
393    String pagename = null;
394  while (true) 
395    {
396    try {
397      System.out.print("Enter LOAD <path-to-page>, RELOAD, GC, or QUIT: ");    
398      String cmdRead = br.readLine();
399      String[] toks = cmdRead.split("\\s+");
400      String cmd = toks[0].toUpperCase();
401      
402      if (cmd.equals("QUIT")) {
403        return;
404        } 
405      else if (cmd.equals("LOAD")) {
406        pagename = toks[1];
407        testLoad(pagemgr, pagename);
408        }   
409      else if (cmd.equals("RELOAD")) {
410        if (pagename == null)
411          System.out.println("Load a page first.....");
412        else
413          testLoad(pagemgr, pagename);
414        }   
415      else if (cmd.equals("GC")) {
416        System.gc();
417        //deprecated
418        //System.runFinalization();
419        }
420      } //try
421    catch (Throwable e) {
422      e.printStackTrace();
423      }
424    } //while
425  } //main
426
427private static void testLoad(PageMgr pagemgr, String pagename) throws Exception
428  {
429  Page p = pagemgr.getPage(pagename);
430  if (p != null) 
431    System.out.println(ClassUtil.getClassLoaderInfo(p));
432  else
433    System.out.println("Could not load page. getPage() returned null");
434  }
435
436}