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 <tt>/WEB-INF/classes</tt>, <tt>/WEB-INF/lib</tt> 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 <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*/ 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\tdocroot: ", docroot, 101 "\n\t\tscratchroot: ", scratchdir, "\n\t\tclasspath: ", classpath); 102 } 103 104/* 105Internally called by pageservlet when it's unloaded. ensures that the 106destroy() method of every loaded page is called. 107*/ 108void 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/** 118Returns 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*/ 124public 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/* 291a.mp -> a_mp 292b.mp -> b_mp 293c.mp -> c_mp 294p/q.mp -> p_q.mp 2955!#.mp -> 5__mp ---> name clash 2965!!.mp -> 5__mp ---> name clash 297 298So a simplistic mapping(any bad classname chars--> '_') will not work. 299We therefore hex-encode every special char. 300*/ 301static 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/** 376Interactive page manager use/testing. 377*/ 378public 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 422private 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}