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        }
219      catch (IOException e) {
220        //the parser may write a partially/badly written file
221        //if the parse failed.
222        if (javafile.exists()) {
223          javafile.delete();
224          }
225        throw e; //rethrow the parse exception
226        }
227        
228      java_modified   = javafile.lastModified();  //since newly parsed
229      }
230  
231    boolean forceReload = false;
232    //Java source could be generated or hacked by hand
233    // if nothing needs compiling, then we still need to load the
234    // page the first time it's accessed
235    if ( class_modified == 0L || java_modified > class_modified) 
236      {
237      if (dbg) System.out.format("java_mod > class_mod, compiling the java source.........\n");
238      
239      log.info("COMPILING page:", javafile.getPath());
240      
241      //src_encoding can be null, that's fine.
242      PageCompiler pc = new PageCompiler(javafile, classpath, src_encoding);
243      
244      if (! pc.compile())
245        throw new ParseException(pc.getError());
246      
247      forceReload = true; //since we recompiled, we reload even if
248                //page exists in the page cache
249      }
250
251    final boolean page_in_map = pagemap.containsKey(contextRelativePath);
252    
253    if (forceReload || ! page_in_map)
254      {
255      PageClassLoader loader = new PageClassLoader(/*scratchdir*/);
256      Class c = loader.loadClass(
257        Page.PACKAGE_NAME + "." + classfile.getCanonicalPath());
258      
259      if (dbg) System.out.println("class = " + c);
260      page = (Page) c.newInstance();
261      if (dbg) System.out.println("page = " + page);
262      page.init(servlet, contextRelativePath);  
263      
264      //the pagemap uses contextRelativePath so that the
265      //we store /foo/bar.mp and /bar.mp differently.
266      //Also there should be a separate instance of 
267      //PageServlet per context/WEB-INF/web.xml, so
268      //different contexts will get different pageservlets
269      //and hence different pagemgr's (and the pagemap
270      //within each pagemgr will be different, hence
271      //same path names within each context won't stomp
272      //over each other).
273      
274      if (page_in_map) {
275        Page oldpage = (Page) pagemap.get(contextRelativePath);
276        oldpage.destroy();
277        }
278      //replace old page always
279      pagemap.put(contextRelativePath, page);
280      }
281    else{
282      page = (Page) pagemap.get(contextRelativePath);
283      }
284    }   
285  
286  if (dbg) log.bug("Returning PAGE=", page);
287  return page;
288  }
289
290/*
291a.mp  ->  a_mp
292b.mp  ->  b_mp
293c.mp  ->  c_mp
294p/q.mp  ->  p_q.mp
2955!#.mp ->   5__mp   ---> name clash 
2965!!.mp ->   5__mp   ---> name clash
297
298So a simplistic mapping(any bad classname chars--> '_') will not work.
299We therefore hex-encode every special char.
300*/
301static String getClassNameFromPageName(String dir, String page) throws IOException
302  {
303  if (dbg) System.out.println("getClassNameFromPageName(): dir=["+dir+"]; page=["+page+"]");
304    
305  StringBuilder buf = new StringBuilder();
306  char c;
307  
308  /*
309  url=/$/y.mp  dir=/$/ or possibly $/  --> name HH_y.mp  [HH=hex]
310  url=/y.mp  dir=/           --> name y.mp 
311  we don't want our names to always start with _ because that's hokey
312  */
313  if (dir.length() > 0)
314    {
315    boolean skip = false;
316    c = dir.charAt(0);
317    
318    if (c == '/' || c == File.separatorChar)
319      skip = true;      
320    
321    if (! skip)
322      {
323      if (! Character.isJavaIdentifierStart(c))
324        buf.append(Integer.toHexString(c));
325      else 
326        buf.append(c);
327      }
328      
329    if (dbg) System.out.println("buf3="+buf.toString());
330    
331    for (int n = 1; n < dir.length(); n++) 
332      {
333      c = dir.charAt(n);
334      
335      if (c == '/' || c == File.separatorChar)
336        c = '_';
337        
338      if (! Character.isJavaIdentifierPart(c))
339        buf.append(Integer.toHexString(c));
340      else 
341        buf.append(c);  
342      
343      if (dbg) System.out.println("buf4="+buf.toString());
344      }
345    }
346    
347  int dotpos = page.indexOf(".");
348  if (dotpos != -1)
349    page = page.substring(0,dotpos);
350  
351  c = page.charAt(0);
352
353  if (! Character.isJavaIdentifierPart(c))
354    buf.append(Integer.toHexString(c));
355  else 
356    buf.append(c);  
357  
358  for (int n = 1; n < page.length(); n++) 
359    {
360    c = page.charAt(n);
361    if (Character.isJavaIdentifierPart(c)) {
362      buf.append(c);
363      }
364    else{
365      if (dbg) System.out.println(">>>>>>> " + c + " -> " + Integer.toHexString(c));
366      buf.append(Integer.toHexString(c));
367      }
368    }
369  
370  buf.append("_mp");
371  return buf.toString();
372  }
373
374    
375/**
376Interactive page manager use/testing.
377*/
378public static void main (String args[]) throws Exception
379  {
380  Args myargs = new Args(args);
381  myargs.setUsage("java fc.web.page.PageMgr -docroot path-to-docroot-dir (. for cwd)] [-scratchroot path-to-scratchdir  (default .)]");
382  String docroot = myargs.getRequired("docroot");
383  String scratchroot = myargs.get("scratchroot", ".");
384  PageMgr pagemgr = new PageMgr(null,
385    new File(docroot), new File(scratchroot), Log.getDefault());
386  
387  //from a jdk techtip
388    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
389    String pagename = null;
390  while (true) 
391    {
392    try {
393      System.out.print("Enter LOAD <path-to-page>, RELOAD, GC, or QUIT: ");    
394      String cmdRead = br.readLine();
395      String[] toks = cmdRead.split("\\s+");
396      String cmd = toks[0].toUpperCase();
397      
398      if (cmd.equals("QUIT")) {
399        return;
400        } 
401      else if (cmd.equals("LOAD")) {
402        pagename = toks[1];
403        testLoad(pagemgr, pagename);
404        }   
405      else if (cmd.equals("RELOAD")) {
406        if (pagename == null)
407          System.out.println("Load a page first.....");
408        else
409          testLoad(pagemgr, pagename);
410        }   
411      else if (cmd.equals("GC")) {
412        System.gc();
413        System.runFinalization();
414        }
415      } //try
416    catch (Throwable e) {
417      e.printStackTrace();
418      }
419    } //while
420  } //main
421
422private static void testLoad(PageMgr pagemgr, String pagename) throws Exception
423  {
424  Page p = pagemgr.getPage(pagename);
425  if (p != null) 
426    System.out.println(ClassUtil.getClassLoaderInfo(p));
427  else
428    System.out.println("Could not load page. getPage() returned null");
429  }
430
431}