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  log       logging destination.
098*/
099public PageClassLoader(Log log) 
100  {
101  this.log = log;
102  }
103
104/**
105Calls {@link PageClassLoader} with the logger set to:
106<code>fc.web.page.PageClassLoader</code>
107*/
108public PageClassLoader() 
109  {
110  this.log = Log.getDefault();
111  }
112
113Map class_file_paths = new HashMap();
114
115/**
116Loads a class corresponding to a page (i.e., a name that starts
117with <code>Page.PackageName</code> from. Delegates the loading of all 
118other classes to the parent classloader (typically the system
119classloader).
120<p>
121If the page suffix is <code>.mp</code>, then a name such as
122<code>foo/bar/baz.page</code> is loaded from the scratch dir (from
123<code>&lt;scratchdir&gt;/foo/bar/baz.class</code>).
124
125@param  name  a relative page path-name (relative to the page 
126        root directory), for example <code>foo/bar/my.page</code>
127        or <code>./my.page</code>
128*/
129public Class loadClass(String name, boolean resolve) throws ClassNotFoundException
130  {
131  if (dbg) log.bug("enter loadClass(): name=", name, " resolve=", resolve);
132  
133  //we could implement findClass() instead of loadClass
134  if (name.startsWith(Page.PACKAGE_NAME))
135    {
136    Class c = findLoadedClass(name);
137    if (c != null)
138      return c ;
139      
140    if (dbg) log.bug("Name starts with:[", Page.PACKAGE_NAME, "], loading page: ", name);
141    final int pkgnamelen =  Page.PACKAGE_NAME.length() + 1; //"foo.pkg."
142  
143    name = name.substring(pkgnamelen, name.length());
144  
145    if (dbg) System.out.println("[A] name=" + name);
146    //for inner, anonymous classes
147    //1) pagemgr first says: load  "molly.page.foo.class"  
148    //2) foo.mp contains a anonymous inner class foo.mp$1
149    //3) this method is called again with "molly.page.foo.mp$1" 
150    //   as the argument
151    //4) we need to load "foo.mp$1.class"
152  
153    if (! name.endsWith(".class")) 
154      {
155      if (dbg) System.out.println("[B]");
156      String outer_class_name = name.substring(0, name.indexOf("$"));
157      if (dbg) System.out.println("[C] outer=" + outer_class_name);
158      String path = (String) class_file_paths.get(outer_class_name);
159      if (dbg) System.out.println("[D] path=" + path);
160      if (path == null) {
161        throw new ClassNotFoundException("Internal error: cannot load anonymous/inner classe: " + name);
162        } 
163  
164      name = path + name + ".class";
165      if (dbg) log.warn("anonname=",name);
166      }
167    else{   
168      if (dbg) System.out.println("[E]");
169      String dirname = StringUtil.dirName(name); //has ending '/'
170      String filename = StringUtil.fileName(name);
171      filename = filename.substring(0, filename.indexOf(".class"));
172      class_file_paths.put(filename, dirname);
173      if (dbg) log.bug("name=",name);
174      if (dbg) log.bug("dirname=",dirname);
175      }
176      
177    c  = readClassFromDisk(new File(name));
178    
179    if (resolve) {
180      resolveClass(c);       
181      }
182    return c;
183    }
184    
185  if (dbg) log.bug("Delegating to thread-context classloader to load class: ", name);
186  //we cannot use the parent classloader because it will not have
187  //classes inside of WEB-INF (the parent classloader is a jvm level
188  //classloader). We need to use the servlet containers classloader
189  //instead.
190  //return super.loadClass(name, resolve);
191  return Thread.currentThread().getContextClassLoader().loadClass(name);
192  }
193
194Class readClassFromDisk(File classfile) throws ClassNotFoundException
195  {
196  Argcheck.istrue(classfile.exists(), "The specified file does not exist: " + classfile.getAbsolutePath());
197  
198  byte[] buf = null;
199  
200  try {   
201    if (dbg) log.bug("Using filename:", classfile);
202    
203    buf = IOUtil.fileToByteArray(classfile);
204    
205    if (buf == null) {
206      throw new ClassNotFoundException(classfile.getPath());
207      }
208      
209    } 
210  catch (Exception e) {
211    throw new ClassNotFoundException(classfile.getPath());
212    }
213  
214  /* 
215  definePackage(Page.PACKAGE_NAME, null, null, null, null, null, null, null);
216
217  this works ONCE per instance of the classloader -- will crap out
218  if we use the same instance to define more than 1 class. defining
219  a package is not needed at all, but may be nice to have for logging
220  purposes/setting logging levels. If we have an inner class, this
221  method will be invoked more than once (one per inner class which
222  uses this same classloader). 
223  
224  All of this is very obscure and if this causes problems, remove.
225  */
226  if (getPackage(Page.PACKAGE_NAME) == null)
227    definePackage(Page.PACKAGE_NAME, null, null, null, null, null, null, null);
228
229  Class c = defineClass(null, buf, 0, buf.length);
230  
231  return c;
232  }
233
234public static void main (String args[])  throws Exception
235  {
236  Args myargs = new Args(args);
237  myargs.setUsage("java " + ClassUtil.getClassName() + 
238    " -file full-path-to-class-file");
239  String page = Page.PACKAGE_NAME + "." + myargs.getRequired("file");
240  
241  PageClassLoader cl  = new PageClassLoader();
242  cl.log.setLevel("debug");
243  
244  System.out.println("Loading java.lang.object: ");
245  Class c = cl.loadClass("java.lang.Object");
246  System.out.println(ClassUtil.getClassLoaderInfo(c));
247
248  System.out.println("Loading " + page);  
249  c = cl.loadClass(page);
250  System.out.println(ClassUtil.getClassLoaderInfo(c));
251  System.out.println("Package for this class: " + c.getPackage());
252  }
253
254}