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 }