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 }