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); 
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    /*
291    a.mp  ->   a_mp
292    b.mp  ->   b_mp
293    c.mp  ->   c_mp
294    p/q.mp  ->  p_q.mp
295    5!#.mp ->  5__mp   ---> name clash 
296    5!!.mp ->   5__mp   ---> name clash
297    
298    So a simplistic mapping(any bad classname chars--> '_') will not work.
299    We therefore hex-encode every special char.
300    */
301    static 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    /**
376    Interactive page manager use/testing.
377    */
378    public 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    
422    private 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    }