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 if (src_encoding == null) { 219 src_encoding = Page.DEFAULT_SRC_ENCODING; 220 } 221 } 222 catch (IOException e) { 223 //the parser may write a partially/badly written file 224 //if the parse failed. 225 if (javafile.exists()) { 226 javafile.delete(); 227 } 228 throw e; //rethrow the parse exception 229 } 230 231 java_modified = javafile.lastModified(); //since newly parsed 232 } 233 234 boolean forceReload = false; 235 //Java source could be generated or hacked by hand 236 // if nothing needs compiling, then we still need to load the 237 // page the first time it's accessed 238 if ( class_modified == 0L || java_modified > class_modified) 239 { 240 if (dbg) System.out.format("java_mod > class_mod, compiling the java source.........\n"); 241 242 log.info("COMPILING page:", javafile.getPath()); 243 244 //src_encoding can be null, that's fine. 245 PageCompiler pc = new PageCompiler(javafile, classpath, src_encoding); 246 247 if (! pc.compile()) 248 throw new ParseException(pc.getError()); 249 250 forceReload = true; //since we recompiled, we reload even if 251 //page exists in the page cache 252 } 253 254 final boolean page_in_map = pagemap.containsKey(contextRelativePath); 255 256 if (forceReload || ! page_in_map) 257 { 258 PageClassLoader loader = new PageClassLoader(/*scratchdir*/); 259 Class c = loader.loadClass( 260 Page.PACKAGE_NAME + "." + classfile.getCanonicalPath()); 261 262 if (dbg) System.out.println("class = " + c); 263 page = (Page) c.newInstance(); 264 if (dbg) System.out.println("page = " + page); 265 page.init(servlet, contextRelativePath); 266 267 //the pagemap uses contextRelativePath so that the 268 //we store /foo/bar.mp and /bar.mp differently. 269 //Also there should be a separate instance of 270 //PageServlet per context/WEB-INF/web.xml, so 271 //different contexts will get different pageservlets 272 //and hence different pagemgr's (and the pagemap 273 //within each pagemgr will be different, hence 274 //same path names within each context won't stomp 275 //over each other). 276 277 if (page_in_map) { 278 Page oldpage = (Page) pagemap.get(contextRelativePath); 279 oldpage.destroy(); 280 } 281 //replace old page always 282 pagemap.put(contextRelativePath, page); 283 } 284 else{ 285 page = (Page) pagemap.get(contextRelativePath); 286 } 287 } 288 289 if (dbg) log.bug("Returning PAGE=", page); 290 return page; 291 } 292 293/* 294a.mp -> a_mp 295b.mp -> b_mp 296c.mp -> c_mp 297p/q.mp -> p_q.mp 2985!#.mp -> 5__mp ---> name clash 2995!!.mp -> 5__mp ---> name clash 300 301So a simplistic mapping(any bad classname chars--> '_') will not work. 302We therefore hex-encode every special char. 303*/ 304static String getClassNameFromPageName(String dir, String page) throws IOException 305 { 306 if (dbg) System.out.println("getClassNameFromPageName(): dir=["+dir+"]; page=["+page+"]"); 307 308 StringBuilder buf = new StringBuilder(); 309 char c; 310 311 /* 312 url=/$/y.mp dir=/$/ or possibly $/ --> name HH_y.mp [HH=hex] 313 url=/y.mp dir=/ --> name y.mp 314 we don't want our names to always start with _ because that's hokey 315 */ 316 if (dir.length() > 0) 317 { 318 boolean skip = false; 319 c = dir.charAt(0); 320 321 if (c == '/' || c == File.separatorChar) 322 skip = true; 323 324 if (! skip) 325 { 326 if (! Character.isJavaIdentifierStart(c)) 327 buf.append(Integer.toHexString(c)); 328 else 329 buf.append(c); 330 } 331 332 if (dbg) System.out.println("buf3="+buf.toString()); 333 334 for (int n = 1; n < dir.length(); n++) 335 { 336 c = dir.charAt(n); 337 338 if (c == '/' || c == File.separatorChar) 339 c = '_'; 340 341 if (! Character.isJavaIdentifierPart(c)) 342 buf.append(Integer.toHexString(c)); 343 else 344 buf.append(c); 345 346 if (dbg) System.out.println("buf4="+buf.toString()); 347 } 348 } 349 350 int dotpos = page.indexOf("."); 351 if (dotpos != -1) 352 page = page.substring(0,dotpos); 353 354 c = page.charAt(0); 355 356 if (! Character.isJavaIdentifierPart(c)) 357 buf.append(Integer.toHexString(c)); 358 else 359 buf.append(c); 360 361 for (int n = 1; n < page.length(); n++) 362 { 363 c = page.charAt(n); 364 if (Character.isJavaIdentifierPart(c)) { 365 buf.append(c); 366 } 367 else{ 368 if (dbg) System.out.println(">>>>>>> " + c + " -> " + Integer.toHexString(c)); 369 buf.append(Integer.toHexString(c)); 370 } 371 } 372 373 buf.append("_mp"); 374 return buf.toString(); 375 } 376 377 378/** 379Interactive page manager use/testing. 380*/ 381public static void main (String args[]) throws Exception 382 { 383 Args myargs = new Args(args); 384 myargs.setUsage("java fc.web.page.PageMgr -docroot path-to-docroot-dir (. for cwd)] [-scratchroot path-to-scratchdir (default .)]"); 385 String docroot = myargs.getRequired("docroot"); 386 String scratchroot = myargs.get("scratchroot", "."); 387 PageMgr pagemgr = new PageMgr(null, 388 new File(docroot), new File(scratchroot), Log.getDefault()); 389 390 //from a jdk techtip 391 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); 392 String pagename = null; 393 while (true) 394 { 395 try { 396 System.out.print("Enter LOAD <path-to-page>, RELOAD, GC, or QUIT: "); 397 String cmdRead = br.readLine(); 398 String[] toks = cmdRead.split("\\s+"); 399 String cmd = toks[0].toUpperCase(); 400 401 if (cmd.equals("QUIT")) { 402 return; 403 } 404 else if (cmd.equals("LOAD")) { 405 pagename = toks[1]; 406 testLoad(pagemgr, pagename); 407 } 408 else if (cmd.equals("RELOAD")) { 409 if (pagename == null) 410 System.out.println("Load a page first....."); 411 else 412 testLoad(pagemgr, pagename); 413 } 414 else if (cmd.equals("GC")) { 415 System.gc(); 416 System.runFinalization(); 417 } 418 } //try 419 catch (Throwable e) { 420 e.printStackTrace(); 421 } 422 } //while 423 } //main 424 425private static void testLoad(PageMgr pagemgr, String pagename) throws Exception 426 { 427 Page p = pagemgr.getPage(pagename); 428 if (p != null) 429 System.out.println(ClassUtil.getClassLoaderInfo(p)); 430 else 431 System.out.println("Could not load page. getPage() returned null"); 432 } 433 434}