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.util.pagetemplate;
007    
008    import java.util.*;
009    import java.io.*;
010    
011    import fc.io.*;
012    import fc.util.*;
013    
014    /**
015    A classloader that loads templates. Each page is loaded by it's own 
016    separate ClassLoader.
017    <p>
018    If a page changes on disk (this is tracked by {@link TemplateMgr}), then the
019    new page is loaded by a new instance of this class.
020    @author hursh jain
021    */
022    public class TemplateClassLoader extends ClassLoader 
023    {
024    /*
025    We don't need to use definePackage for our classloaders,
026    there are no manifest based versions associated with pages.
027    
028    Say a java file in package foo.bar exists in: 
029      /mydir/foo/bar/baz.java
030    
031    Then running "javac -classpath /mydir foo.bar.baz.java" will 
032    result in: 
033      /mydir/foo/bar/baz.class
034    
035    Now say: 
036      cd /mydir/foo/bar/
037      java -classpath "." foo.bar.baz
038      
039    This will cause an error since "java" actually checks to
040    see that the classpath it _used_ is the same as the classpath
041    in the file it found. (in this example, java used "." but found
042    foo.bar.baz.class).
043    
044    However, we can read a byte[] corresponding to a .class file
045    from anywhere on the disk, network, database and this has 
046    (typically for custom loaders) nothing to do with the classpath.
047    
048    Therefore, when implenting a custom classloader, we can
049    maintain a directory structure for generated class files OR
050    put all generated class files in one directory, as say
051    /mydir/foo_bar_baz.class etc OR do both.
052    
053    GENERATED TEMPLATE CLASSES ARE ALL IN THE SAME PACKAGE (molly.pagetemplate) AND
054    ARE ORGANIZED IN A DIRECTORY STRUCTURE THAT MIRRORS THE SOURCE
055    HEIRARCHY. THIS MAKES GENERATED .java PAGES (CORRESPONDING TO THE SOURCE
056    .mp) EASIER TO FIND FOR HUMANS.
057    
058    Instead of simply implementing findClass, we _could_ also 
059    override loadClass(). This allows us to warn/error out if
060    the parent classloader was able to load the page's class
061    file (which should never happen since the classpath containing
062    the template .class files should NOT be in the system classpath).
063    If it IS in the system classpath, page re-loading will not
064    work, which is an error for our purposes. 
065    
066    We still need to delegate to a parent classloader for non-template
067    classes referenced from a page. To simplify things, I've used
068    the delegation model here and will give the parent classloader
069    first crack at loading everything....this is the way it should
070    always be anyway.
071    */
072    
073    private static final boolean dbg = false;
074    Log   log;
075    
076    /**
077    @param  scratch     absolute path to the scratch dir
078    @param  log       logging destination.
079    */
080    public TemplateClassLoader(Log log) 
081      {
082      this.log = log;
083      }
084    
085    /**
086    Calls {@link TemplateClassLoader} with the logger set to:
087    <tt>fc.web.page.TemplateClassLoader</tt>
088    */
089    public TemplateClassLoader() 
090      {
091      this.log = Log.getDefault();
092      }
093    
094    Map class_file_paths = new HashMap();
095    
096    /**
097    Loads a class corresponding to a page (i.e., a name that starts
098    with {@link Page.PackageName} from. Delegates the loading of all 
099    other classes to the parent classloader (typically the system
100    classloader).
101    <p>
102    If the page suffix is <tt>.mp</tt>, then a name such as
103    <tt>foo/bar/baz.page</tt> is loaded from the scratch dir (from
104    <tt><scratchdir>/foo/bar/baz.class</tt>).
105    
106    @param  name  a relative page path-name (relative to the page 
107            root directory), for example <tt>foo/bar/my.page</tt>
108            or <tt>./my.page</tt>
109    */
110    public Class loadClass(String name, boolean resolve) throws ClassNotFoundException
111      {
112      if (dbg) log.bug("enter loadClass(): name=", name, " resolve=", resolve);
113      
114      //we could implement findClass() instead of loadClass
115      if (name.startsWith(TemplatePage.PACKAGE_NAME))
116        {
117        Class c = findLoadedClass(name);
118        if (c != null)
119          return c ;
120          
121        if (dbg) log.bug("Name starts with:[", TemplatePage.PACKAGE_NAME, "], loading page: ", name);
122        final int pkgnamelen =  TemplatePage.PACKAGE_NAME.length() + 1; //"foo.pkg."
123      
124        name = name.substring(pkgnamelen, name.length());
125      
126        if (dbg) System.out.println("[A] name=" + name);
127        //for inner, anonymous classes
128        //1) pagemgr first says: load  "molly.page.foo.class"  
129        //2) foo.mp contains a anonymous inner class foo.mp$1
130        //3) this method is called again with "molly.page.foo.mp$1" 
131        //   as the argument
132        //4) we need to load "foo.mp$1.class"
133      
134        if (! name.endsWith(".class")) 
135          {
136          if (dbg) System.out.println("[B]");
137          String outer_class_name = name.substring(0, name.indexOf("$"));
138          if (dbg) System.out.println("[C] outer=" + outer_class_name);
139          String path = (String) class_file_paths.get(outer_class_name);
140          if (dbg) System.out.println("[D] path=" + path);
141          if (path == null) {
142            throw new ClassNotFoundException("Internal error: cannot load anonymous/inner classe: " + name);
143            } 
144      
145          name = path + name + ".class";
146          if (dbg) log.warn("anonname=",name);
147          }
148        else{   
149          if (dbg) System.out.println("[E]");
150          String dirname = StringUtil.dirName(name); //has ending '/'
151          String filename = StringUtil.fileName(name);
152          filename = filename.substring(0, filename.indexOf(".class"));
153          class_file_paths.put(filename, dirname);
154          if (dbg) log.bug("name=",name);
155          if (dbg) log.bug("dirname=",dirname);
156          }
157          
158        c  = readClassFromDisk(new File(name));
159        
160        if (resolve) {
161          resolveClass(c);       
162          }
163        return c;
164        }
165        
166      if (dbg) log.bug("Delegating to thread-context classloader to load class: ", name);
167      //we cannot use the parent classloader because it will not have
168      //classes inside of WEB-INF (the parent classloader is a jvm level
169      //classloader). We need to use the servlet containers classloader
170      //instead.
171      //return super.loadClass(name, resolve);
172      return Thread.currentThread().getContextClassLoader().loadClass(name);
173      }
174    
175    Class readClassFromDisk(File classfile) throws ClassNotFoundException
176      {
177      Argcheck.istrue(classfile.exists(), "The specified file does not exist: " + classfile.getAbsolutePath());
178      
179      byte[] buf = null;
180      
181      try {   
182        if (dbg) log.bug("Using filename:", classfile);
183        
184        buf = IOUtil.fileToByteArray(classfile);
185        
186        if (buf == null) {
187          throw new ClassNotFoundException(classfile.getPath());
188          }
189          
190        } 
191      catch (Exception e) {
192        throw new ClassNotFoundException(classfile.getPath());
193        }
194      
195      /* 
196      definePackage(Page.PACKAGE_NAME, null, null, null, null, null, null, null);
197    
198      this works ONCE per instance of the classloader -- will crap out
199      if we use the same instance to define more than 1 class. defining
200      a package is not needed at all, but may be nice to have for logging
201      purposes/setting logging levels. If we have an inner class, this
202      method will be invoked more than once (one per inner class which
203      uses this same classloader). 
204      
205      All of this is very obscure and if this causes problems, remove.
206      */
207      if (getPackage(TemplatePage.PACKAGE_NAME) == null)
208        definePackage(TemplatePage.PACKAGE_NAME, null, null, null, null, null, null, null);
209    
210      Class c = defineClass(null, buf, 0, buf.length);
211      
212      return c;
213      }
214    
215    public static void main (String args[])  throws Exception
216      {
217      Args myargs = new Args(args);
218      myargs.setUsage("java " + ClassUtil.getClassName() + 
219        " -file full-path-to-class-file");
220      String page = TemplatePage.PACKAGE_NAME + "." + myargs.getRequired("file");
221      
222      TemplateClassLoader cl  = new TemplateClassLoader();
223      cl.log.setLevel("debug");
224      
225      System.out.println("Loading java.lang.object: ");
226      Class c = cl.loadClass("java.lang.Object");
227      System.out.println(ClassUtil.getClassLoaderInfo(c));
228    
229      System.out.println("Loading " + page);  
230      c = cl.loadClass(page);
231      System.out.println(ClassUtil.getClassLoaderInfo(c));
232      System.out.println("Package for this class: " + c.getPackage());
233      }
234    
235    }