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.io.*; 009import java.util.*; 010 011import fc.util.*; 012import fc.io.*; 013 014/** 015Manages pages. Pages are found below a web document root directory. Pages 016are compiled as needed and the resulting class file is loaded/run. If a 017page is changed, it is automatically recompiled, reloaded and rerun. If the 018page has a compilation error, that page remains unloaded until the error is 019fixed. 020<p> 021A new PageMgr should be instantiated for each unique root directory (for 022example with multiple virtual hosts or users, each having their own root 023directory). 024 025@author hursh jain 026*/ 027public final class PageMgr 028{ 029private static final boolean dbg = false; 030 031File docroot; 032File scratchdir; //for compiled pages. 033Map pagemap = new HashMap(); 034Log log; 035String classpath; 036PageServlet 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. 039String docrootStr; 040String scratchdirStr; 041 042/** 043Constructs a new Page manager. The page manager will use the system 044classpath and <code>/WEB-INF/classes</code>, <code>/WEB-INF/lib</code> as the 045classpath for complilation. Pages can refer to any class found in those 046locations. 047 048@param servlet the Molly Servlet. This is optional and can be 049 <code>null</code> 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 <code>page-->java</code> file will be created. 057@param log a logging destination. 058 059*/ 060public 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\t\tdocroot: ", docroot, 101 "\n\t\t\tscratchroot: ", scratchdir, 102 "\n\t\t\tclasspath: ", classpath); 103 } 104 105/* 106Internally called by pageservlet when it's unloaded. ensures that the 107destroy() method of every loaded page is called. 108*/ 109void destroy() 110 { 111 Iterator i = pagemap.values().iterator(); 112 while (i.hasNext()) { 113 Page p = (Page) i.next(); 114 p.destroy(); 115 } 116 } 117 118/** 119Returns the {@link Page} corresponding the the page path. 120 121@param contextRelativePath path relative to the servlet context 122 (e.g.: "/foo/bar/baz.mp"), the leading 123 '/' is optional. 124*/ 125public Page getPage(String contextRelativePath) throws Exception 126 { 127 Argcheck.notnull(contextRelativePath, "Internal error: the contextRelativePath parameter was null"); 128 Page page = null; 129 130 final CharArrayWriter fbuf = new CharArrayWriter(128); 131 132 //File docroot_pagefile = new File(docroot, contextRelativePagePath); 133 //->micro-optimization 134 fbuf.append(docrootStr).append(File.separator).append(contextRelativePath); 135 final File docroot_pagefile = new File(fbuf.toString()); 136 fbuf.reset(); 137 //->end mo 138 139 if (! docroot_pagefile.exists()) { 140 if (dbg) System.out.println("page: " + docroot_pagefile + " does not exist. Returning null page"); 141 return null; 142 } 143 144 final String pagefile = StringUtil.fileName(contextRelativePath); 145 146 /* this can be '/' if page = /foo.mp or '' if page = foo.mp */ 147 final String pagedir = StringUtil.dirName(contextRelativePath); 148 149 if (! (pagedir.equals("/") || pagedir.equals("") )) { 150 //File pagedirFile = new File(scratchdir, pagedir); 151 //->micro-optimization 152 fbuf.append(scratchdirStr).append(File.separator).append(pagedir); 153 File pagedirFile = new File(fbuf.toString()); 154 fbuf.reset(); 155 //->end mo 156 157 //we need to create this directory otherwise print/file writers 158 //that try to write a file within that directory will crap out. 159 if (! pagedirFile.exists()) { 160 if (dbg) System.out.println("Creating directory: " + pagedirFile); 161 pagedirFile.mkdirs(); 162 } 163 } 164 165 String classname = getClassNameFromPageName(pagedir, pagefile); 166 167 /* 168 File javafile = new File(scratchdir, 169 pagedir + File.separator + classname + ".java"); 170 171 File classfile = new File(scratchdir, 172 pagedir + File.separator + classname + ".class"); 173 */ 174 //->micro-optimization 175 fbuf.append(scratchdirStr).append(pagedir).append(File.separator) 176 .append(classname).append(".java"); 177 final File javafile = new File(fbuf.toString()); 178 fbuf.reset(); 179 180 fbuf.append(scratchdirStr).append(pagedir).append(File.separator) 181 .append(classname).append(".class"); 182 final File classfile = new File(fbuf.toString()); 183 fbuf.reset(); 184 //->end mo 185 186 if (dbg) { 187 System.out.println( 188 String.format("contextRelativePath=%s, pagedir=%s, pagefile=%s, javafile=%s, classfile=%s\n", 189 contextRelativePath, pagedir, pagefile, javafile, classfile)); 190 } 191 192 synchronized (this) 193 { 194 String src_encoding = null; 195 long page_modified = docroot_pagefile.lastModified(); 196 long java_modified = javafile.lastModified(); //returns 0 if !exist 197 long class_modified = classfile.lastModified(); //returns 0 if !exist 198 199 if (dbg) 200 { 201 System.out.format( 202 " %-20s %10d\n %-20s %10d\n %-20s %10d\n", 203 "Modified: page:", page_modified, 204 "java:", java_modified, 205 "class:", class_modified); 206 } 207 208 if ( java_modified == 0L || page_modified > java_modified) 209 { 210 if (dbg) System.out.format("page_mod > java_mod, parsing the page.........\n"); 211 PageParser parser = new PageParser( 212 docroot, docroot_pagefile, javafile, classname); 213 214 log.info("PARSING page:", javafile.getPath()); 215 216 try { 217 parser.parse(); 218 src_encoding = parser.getSourceEncoding(); 219 if (src_encoding == null) { 220 src_encoding = Page.DEFAULT_SRC_ENCODING; 221 } 222 } 223 catch (IOException e) { 224 //the parser may write a partially/badly written file 225 //if the parse failed. 226 if (javafile.exists()) { 227 javafile.delete(); 228 } 229 throw e; //rethrow the parse exception 230 } 231 232 java_modified = javafile.lastModified(); //since newly parsed 233 } 234 235 boolean forceReload = false; 236 //Java source could be generated or hacked by hand 237 // if nothing needs compiling, then we still need to load the 238 // page the first time it's accessed 239 if ( class_modified == 0L || java_modified > class_modified) 240 { 241 if (dbg) System.out.format("java_mod > class_mod, compiling the java source.........\n"); 242 243 log.info("COMPILING page:", javafile.getPath()); 244 245 //src_encoding can be null, that's fine. 246 PageCompiler pc = new PageCompiler(javafile, classpath, src_encoding); 247 248 if (! pc.compile()) 249 throw new ParseException(pc.getError()); 250 251 forceReload = true; //since we recompiled, we reload even if 252 //page exists in the page cache 253 } 254 255 final boolean page_in_map = pagemap.containsKey(contextRelativePath); 256 257 if (forceReload || ! page_in_map) 258 { 259 PageClassLoader loader = new PageClassLoader(/*scratchdir*/); 260 Class c = loader.loadClass( 261 Page.PACKAGE_NAME + "." + classfile.getCanonicalPath()); 262 263 if (dbg) System.out.println("class = " + c); 264 page = (Page) c.newInstance(); 265 if (dbg) System.out.println("page = " + page); 266 page.init(servlet, contextRelativePath); 267 268 //the pagemap uses contextRelativePath so that the 269 //we store /foo/bar.mp and /bar.mp differently. 270 //Also there should be a separate instance of 271 //PageServlet per context/WEB-INF/web.xml, so 272 //different contexts will get different pageservlets 273 //and hence different pagemgr's (and the pagemap 274 //within each pagemgr will be different, hence 275 //same path names within each context won't stomp 276 //over each other). 277 278 if (page_in_map) { 279 Page oldpage = (Page) pagemap.get(contextRelativePath); 280 oldpage.destroy(); 281 } 282 //replace old page always 283 pagemap.put(contextRelativePath, page); 284 } 285 else{ 286 page = (Page) pagemap.get(contextRelativePath); 287 } 288 } 289 290 if (dbg) log.bug("Returning PAGE=", page); 291 return page; 292 } 293 294/* 295a.mp -> a_mp 296b.mp -> b_mp 297c.mp -> c_mp 298p/q.mp -> p_q.mp 2995!#.mp -> 5__mp ---> name clash 3005!!.mp -> 5__mp ---> name clash 301 302So a simplistic mapping(any bad classname chars--> '_') will not work. 303We therefore hex-encode every special char. 304*/ 305static String getClassNameFromPageName(String dir, String page) throws IOException 306 { 307 if (dbg) System.out.println("getClassNameFromPageName(): dir=["+dir+"]; page=["+page+"]"); 308 309 StringBuilder buf = new StringBuilder(); 310 char c; 311 312 /* 313 url=/$/y.mp dir=/$/ or possibly $/ --> name HH_y.mp [HH=hex] 314 url=/y.mp dir=/ --> name y.mp 315 we don't want our names to always start with _ because that's hokey 316 */ 317 if (dir.length() > 0) 318 { 319 boolean skip = false; 320 c = dir.charAt(0); 321 322 if (c == '/' || c == File.separatorChar) 323 skip = true; 324 325 if (! skip) 326 { 327 if (! Character.isJavaIdentifierStart(c)) 328 buf.append(Integer.toHexString(c)); 329 else 330 buf.append(c); 331 } 332 333 if (dbg) System.out.println("buf3="+buf.toString()); 334 335 for (int n = 1; n < dir.length(); n++) 336 { 337 c = dir.charAt(n); 338 339 if (c == '/' || c == File.separatorChar) 340 c = '_'; 341 342 if (! Character.isJavaIdentifierPart(c)) 343 buf.append(Integer.toHexString(c)); 344 else 345 buf.append(c); 346 347 if (dbg) System.out.println("buf4="+buf.toString()); 348 } 349 } 350 351 int dotpos = page.indexOf("."); 352 if (dotpos != -1) 353 page = page.substring(0,dotpos); 354 355 c = page.charAt(0); 356 357 if (! Character.isJavaIdentifierPart(c)) 358 buf.append(Integer.toHexString(c)); 359 else 360 buf.append(c); 361 362 for (int n = 1; n < page.length(); n++) 363 { 364 c = page.charAt(n); 365 if (Character.isJavaIdentifierPart(c)) { 366 buf.append(c); 367 } 368 else{ 369 if (dbg) System.out.println(">>>>>>> " + c + " -> " + Integer.toHexString(c)); 370 buf.append(Integer.toHexString(c)); 371 } 372 } 373 374 buf.append("_mp"); 375 return buf.toString(); 376 } 377 378 379/** 380Interactive page manager use/testing. 381*/ 382public static void main (String args[]) throws Exception 383 { 384 Args myargs = new Args(args); 385 myargs.setUsage("java fc.web.page.PageMgr -docroot path-to-docroot-dir (. for cwd)] [-scratchroot path-to-scratchdir (default .)]"); 386 String docroot = myargs.getRequired("docroot"); 387 String scratchroot = myargs.get("scratchroot", "."); 388 PageMgr pagemgr = new PageMgr(null, 389 new File(docroot), new File(scratchroot), Log.getDefault()); 390 391 //from a jdk techtip 392 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); 393 String pagename = null; 394 while (true) 395 { 396 try { 397 System.out.print("Enter LOAD <path-to-page>, RELOAD, GC, or QUIT: "); 398 String cmdRead = br.readLine(); 399 String[] toks = cmdRead.split("\\s+"); 400 String cmd = toks[0].toUpperCase(); 401 402 if (cmd.equals("QUIT")) { 403 return; 404 } 405 else if (cmd.equals("LOAD")) { 406 pagename = toks[1]; 407 testLoad(pagemgr, pagename); 408 } 409 else if (cmd.equals("RELOAD")) { 410 if (pagename == null) 411 System.out.println("Load a page first....."); 412 else 413 testLoad(pagemgr, pagename); 414 } 415 else if (cmd.equals("GC")) { 416 System.gc(); 417 //deprecated 418 //System.runFinalization(); 419 } 420 } //try 421 catch (Throwable e) { 422 e.printStackTrace(); 423 } 424 } //while 425 } //main 426 427private static void testLoad(PageMgr pagemgr, String pagename) throws Exception 428 { 429 Page p = pagemgr.getPage(pagename); 430 if (p != null) 431 System.out.println(ClassUtil.getClassLoaderInfo(p)); 432 else 433 System.out.println("Could not load page. getPage() returned null"); 434 } 435 436}