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