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