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.servlet; 007 008 import javax.servlet.*; 009 import javax.servlet.http.*; 010 import java.io.*; 011 import java.util.*; 012 import java.sql.*; 013 014 import fc.io.*; 015 import fc.jdbc.*; 016 import fc.util.*; 017 import fc.util.cache.*; 018 import fc.web.*; 019 import fc.web.forms.*; 020 021 /** 022 Application level global object running within a servlet web application 023 (i.e., a servlet context). Global webapp data can be stored/retrieved 024 via the put/get methods. 025 <p> 026 Initializes and stores various variables useful for all servlets/pages 027 running in our JVM. Implements {@link javax.servlet.ServletContextListener} 028 and initializes itself when informed by the servlet container's context initialization event. 029 <p> 030 It's optional to use this class. If it is used, it's configured by adding the 031 following to the appropriate sections of WEB-INF/web.xml: 032 <blockquote> 033 <pre> 034 <context-param> 035 <param-name>configfile</param-name> 036 <param-value>app.conf</param-value> 037 </context-param> 038 <context-param> 039 <param-name>appName</param-name> 040 <param-value>some-arbitrary-string-unique-across-<b>all</b>-webapps</param-value> 041 </context-param> 042 043 <listener> 044 <listener-class>fc.web.servlet.WebApp</listener-class> 045 </listener> 046 </pre> 047 </blockquote> 048 <p> 049 <font size="+2"><b>Important</b></font>:<u>If this class is used <font 050 <i>and</i> its initialization is not successful</u>, <font size="+2">it tries to 051 shut down the entire servlet JVM</font> by calling <tt>System.exit</tt>. (The 052 idea being it's better to fail early and safely then continue beyond this 053 point). 054 <p> 055 If used, this class requires the following context configuration 056 parameter: 057 <blockquote> 058 <ul> 059 <li><tt>configfile</tt>: Path/name of the application configuration 060 file. If the path starts with a '/', it is an absolute file system path. 061 Otherwise, it is relative to this context root's WEB-INF directory.</li> 062 <li><tt>appName</tt>: Some arbitrary (but unique) name associated with this 063 webapp</li> 064 </ul> 065 </blockquote> 066 <p> 067 This class can also be subclassed to initialize/contain website specific data 068 and background/helper processing threads. Alternatively, along with this class 069 as-is, additional independent site-specific ServletContextListener classes can 070 be created and used as necessary. 071 072 @author hursh jain 073 */ 074 public class WebApp implements ServletContextListener 075 { 076 //IMPL NOTE: synchronize stuff that is set here with AdminServlet 077 /* 078 Contains all the {@link fc.dbo.ConnectionMgr ConnectionManagers} 079 for this application. 080 */ 081 public Map connectionManagers = new HashMap(); 082 public ConnectionMgr defaultConnectionManager; 083 public long default_dbcache_time = MemoryCache.TWO_MIN; 084 public ThreadLocalCalendar default_tlcal; 085 public ThreadLocalDateFormat default_tldf; 086 public ThreadLocalNumberFormat default_tlnf; 087 public ThreadLocalRandom default_tlrand; 088 public Map appMap = new Hashtable(); //hashtable is sync'ed 089 public Map tlcalMap = new Hashtable(); //ht is sync'ed 090 public Map tldfMap = new Hashtable(); //ht is sync'ed 091 public Map tlnfMap = new Hashtable(); //ht is sync'ed 092 public Map tlrandMap = new Hashtable(); //ht is sync'ed 093 public Map tlMap = new Hashtable(); //ht is sync'ed 094 095 /** 096 A {@link Log} object. Servlets typically create their own 097 loggers (with servlet specific logging levels) but can alternatively 098 use this default appLog. This appLog is used by non-servlet classes 099 such as this class itself, various listeners etc. 100 */ 101 public Log appLog; 102 public PropertyMgr propertyMgr; 103 long cache_time; //in ms 104 105 /** The required 'appName' parameter read from the config file (web.xml) **/ 106 protected String appName; 107 108 /** 109 A (initially empty) Map that servlets can use to store a reference to 110 themselves. This is required because the servlet API has deprecated a 111 similar API call (the servlet API authors are brain damaged 'tards). 112 */ 113 public Map allServletsMap = new HashMap(); 114 private static Map<String, WebApp> instances = new HashMap(); 115 /** this is for WebApps added externally/manually by a servlet via addWebApp. When 116 the context/webapp under which that servlet is running is destroyed, that manually 117 added webapp is also automatically destroyed */ 118 private static Set autoDestroySet = new HashSet(); 119 120 121 /** 122 A no-arg constructor public such that the servlet container can instantiate this class. 123 */ 124 public WebApp() { } 125 126 /* 127 Returns an instance of the webapp configured for this application (or <tt>null</tt> 128 if not yet configured (via the servlet context initialization) or not found. 129 */ 130 public static WebApp getInstance(String appName) 131 { 132 synchronized (WebApp.class) 133 { 134 return instances.get(appName); 135 } 136 } 137 138 /** 139 Basic implementation of the web application cleanup upon context creation. 140 141 If this method is subclassed, the subclassed method must also invoke this 142 method via <tt>super.contextInitialized</tt>. This call should typically 143 be at the beginning of the subclassed method, which allows this implementation 144 to create connections, logs etc, which can then be used by the subclass to 145 finish it's further initialization as needed. 146 */ 147 public void contextInitialized(ServletContextEvent sce) 148 { 149 ServletContext context = sce.getServletContext(); 150 try { 151 String appName = WebUtil.getRequiredParam(context, "appName"); 152 String sconf = WebUtil.getRequiredParam(context, "configfile"); 153 154 addWebAppImpl(context, appName, sconf, this); 155 } 156 catch (ServletException e) { 157 System.err.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); 158 System.err.println("**** ERROR: exception in " + getClass().getName() + " *****"); 159 System.err.println("**** Shutting down the Web Server ****"); 160 System.err.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); 161 System.err.flush(); 162 e.printStackTrace(System.err); //send email to someone here ? 163 164 //There really isn't a good way to stop just this app from working (and 165 //keep other webapps/hosts alive). There isn't any way to throw a 166 //UnavailableException from here, those exceptions are at each individual 167 //servlet level, not webapp/context level. Freaking dumb. Can use flags 168 //and a catch all filter but too much work. Shut this fucker down. 169 170 System.exit(1); 171 } 172 } //~WebApp initialized 173 174 175 /** 176 Basic implementation of the web application cleanup upon context destruction. 177 178 If this method is subclassed, the subclassed method must also invoke this 179 method via <tt>super.contextInitialized</tt>. This invokation should typically 180 be at the <b>end</b> of the subclassed method (which allows the subclass to 181 use connections etc., before they are closed by this superclass method). 182 */ 183 public void contextDestroyed(ServletContextEvent sce) 184 { 185 appLog.info("WebApp [", appName, "] closing at: ", new java.util.Date()); 186 187 cleanup(appName); 188 189 ServletContext context = sce.getServletContext(); 190 String appName2 = null; 191 try { 192 appName2 = WebUtil.getRequiredParam(context, "appName"); 193 if (! this.appName.equals(appName2)) { 194 System.err.println("Weird, appName (" + appName + ") and context.appName(" + appName2 + ") were different"); 195 cleanup(appName2); 196 } 197 } 198 catch (Exception e) { 199 IOUtil.throwableToString(e); 200 } 201 202 //auto destroy (manually added webapps) 203 Iterator it = autoDestroySet.iterator(); 204 while (it.hasNext()) { 205 String name = (String) it.next(); 206 cleanup(name); 207 it.remove(); 208 } 209 } 210 211 private void cleanup(String appName) 212 { 213 WebApp app = (WebApp) instances.remove(appName); 214 215 if (app == null) { 216 return; 217 } 218 219 Iterator it = app.connectionManagers.values().iterator(); 220 221 while (it.hasNext()) { 222 ConnectionMgr cmgr = (ConnectionMgr) it.next(); 223 cmgr.close(); 224 } 225 } 226 227 228 /** 229 Add a new WebApp instance manually. A WebApp is typically specified as a context 230 listener, in a particular <i>web.xml</i>, and configures itself as representing 231 that context wide configuration (common to all servlets and pages running inside 232 that context). There is only 1 instance of the WebApp class that is instantiated 233 per context by the servlet container. 234 <p> 235 However, for REST services and other API's, it is useful to have several API 236 versions, such as <tt>/rest/v1/</tt>, <tt>/rest/v2/</tt>, etc., all of which are 237 tied to seperate servlets in the <b>same</b> webapp. We could also create 238 separate webapps inside separate folders, such as <tt>/w3root/rest/v1</tt>, 239 <tt>/w3root/rest/v2</tt>, each of which would have their own web.xml (and hence 240 separate configuration). 241 <p> 242 However, we sometimes need to access a particular REST api's <b>classes</b> directly 243 from the "root" document webapp (which is running molly pages, etc). If the REST api's 244 are in separate contexts, the root webapp (a different webapp from all the other 245 REST api versioned webapps) cannot access those api classes directly. This becomes 246 a hassle if we want to use our API directly to show results on a molly web page. 247 <p> 248 So we use only <b>one</b> webapp, with separate servlets in that webapp. Example: 249 <tt>RESTServletV1</tt>, <tt>RESTServletV2</tt>, etc., to handle and serve different 250 version of the API. 251 <p> 252 So then, each of these servlets have to be configured as well with connection pools, 253 loggers, etc, each of them specific to a particular servlet/api. This can be done 254 via servlet init parameters, but since the whole point of WebApp is to set up this 255 configuration easily, it is also useful to create a WebApp instance <i>per 256 servlet instance</i> in the same webapp. 257 <p> 258 Servlets can then call this method with different appNames (unique to each servlet) 259 and different configuration files (again unique to each servlet). When the context 260 is destroyed, all these manually added webapps will also be automatically closed. 261 <p> 262 This class must be specified as a listener in web.xml (even if the context wide 263 config file has no configuration data, use an empty config file in that case). 264 <p> 265 PS: If this makes your head hurt, you are in good company. My head hurts too. 266 267 @param context the servlet context the calling servlet is running in 268 @param appName the name of the WebApp to associate with the calling servlet 269 @param conf the configuration file for the WebApp 270 */ 271 public static WebApp addWebApp(ServletContext context, String appName, String sconf) 272 { 273 WebApp app = new WebApp(); 274 addWebAppImpl(context, appName, sconf, app); 275 autoDestroySet.add(appName); 276 277 return app; 278 } 279 280 private static void addWebAppImpl(ServletContext context, 281 String appName, String sconf, WebApp app) 282 { 283 java.util.Date now = Calendar.getInstance().getTime(); 284 System.out.println("*******************************************************************"); 285 System.out.println("fc.web.servlet.WebApp: starting initialization on: " + now); 286 System.out.println("WebApp running at: " + context.getRealPath("")); 287 System.out.println("*******************************************************************"); 288 try { 289 if (instances.containsKey(appName)) 290 { 291 System.err.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); 292 String err = "A webapp with name[" 293 + appName + "] at context [" 294 + context.getRealPath(context.getContextPath()) + "] already exists.."; 295 System.err.println(err); 296 //this *should* prevent any servlet in this context from working. 297 //gentler than System.exit() 298 throw new RuntimeException(err); 299 } 300 301 app.appName = appName; 302 303 Log appLog = Log.get(appName); 304 app.appLog = appLog; 305 306 File conf = new File(sconf); 307 System.out.println("-> WebApp: Configuration file: " + conf.getPath()); 308 309 if (! conf.isAbsolute()) { 310 conf = new File(context.getRealPath("/WEB-INF"), conf.getPath()); 311 } 312 313 PropertyMgr propertyMgr = new FilePropertyMgr(conf); 314 app.propertyMgr = propertyMgr; 315 316 System.out.println("-> WebApp: " + propertyMgr); 317 318 String level = propertyMgr.get("log.level", null); 319 if (level != null) { 320 appLog.setLevel(level); 321 Log.setDefaultLevel(level); 322 } 323 324 ConnectionMgr defaultCmgr = null; 325 326 String dbdefault_str = propertyMgr.get("db.default"); 327 app.appLog.info("WebApp: Default database = ", 328 dbdefault_str == null ? "Not specified" : dbdefault_str); 329 330 String dblist_str = propertyMgr.get("db.list"); 331 app.appLog.info("WebApp: All databases: ", dblist_str == null ? 332 "None specified" : dblist_str); 333 334 if (dblist_str == null) { 335 if (dbdefault_str != null) { 336 throw new IllegalArgumentException("Since a default database was specified, a dblist must also be specified (but was null)"); 337 } 338 } 339 else{ 340 String[] cmgrnames = dblist_str.split(","); 341 for (String dbname : cmgrnames) 342 { 343 dbname = dbname.trim(); 344 String poolsize = propertyMgr.get(dbname + ".pool.size"); 345 String jdbc_url = propertyMgr.get(dbname + ".jdbc.url"); 346 String jdbc_driver = propertyMgr.get(dbname + ".jdbc.driver"); 347 String jdbc_user = propertyMgr.get(dbname + ".jdbc.user"); 348 String jdbc_password = propertyMgr.get(dbname + ".jdbc.password"); 349 String jdbc_catalog = propertyMgr.get(dbname + ".jdbc.catalog", ""); 350 351 ConnectionMgr cmgr = new PooledConnectionMgr( 352 jdbc_url, jdbc_driver, jdbc_user, jdbc_password, 353 jdbc_catalog, Integer.parseInt(poolsize)); 354 355 app.connectionManagers.put(dbname, cmgr); 356 if (dbname.equalsIgnoreCase(dbdefault_str)) 357 app.defaultConnectionManager = cmgr; 358 } 359 } 360 361 String cache_time_str = propertyMgr.get("db.cache_time_seconds"); 362 363 if (cache_time_str != null) { 364 app.cache_time = Long.parseLong(cache_time_str) * 1000; 365 } 366 else{ 367 app.cache_time = app.default_dbcache_time; 368 } 369 370 appLog.info("WebApp: db.cache_time = ", app.cache_time/1000, " seconds"); 371 372 QueryUtil.init(appLog); 373 instances.put(appName, app); 374 375 System.out.println("===> WebApp finished [success]"); 376 System.out.println("*******************************************************************\n"); 377 } 378 catch (Exception e) 379 { 380 System.err.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); 381 System.err.println("**** ERROR: exception in " + app.getClass().getName() + " *****"); 382 System.err.println("**** Shutting down the Web Server ****"); 383 System.err.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); 384 System.err.flush(); 385 e.printStackTrace(System.err); //send email to someone here ? 386 387 //There really isn't a good way to stop just this app from working (and 388 //keep other webapps/hosts alive). There isn't any way to throw a 389 //UnavailableException from here, those exceptions are at each individual 390 //servlet level, not webapp/context level. Freaking dumb. Can use flags 391 //and a catch all filter but too much work. Shut this fucker down. 392 393 System.exit(1); 394 } 395 } 396 397 398 /** 399 Returns the property manager associated with this WebApp (properties of the app 400 configuration file can be read via this property mgr) 401 */ 402 public PropertyMgr getPropertyMgr() 403 { 404 return propertyMgr; 405 } 406 407 /** 408 Returns the connection manager corresponding to the database 409 name. (specified in the dblist property of app.conf). 410 411 @throws IllegalArgumentException if the specified database is not found 412 */ 413 public ConnectionMgr getConnectionMgr(String databasename) 414 { 415 ConnectionMgr cm = (ConnectionMgr) connectionManagers.get(databasename); 416 if (cm == null) { 417 throw new IllegalArgumentException("The specified connectionManager [" + databasename + "] does not exist or has not been initialized."); 418 } 419 return cm; 420 } 421 422 /** 423 Returns the connection manager corresponding to the default database 424 name. (specified in the dbdefault property of app.conf). Returns <tt>null</tt> 425 if no default database has been initialized. 426 */ 427 public ConnectionMgr getConnectionMgr() 428 { 429 return defaultConnectionManager; 430 } 431 432 /* 433 returns a connection from the connection pool to the database specified. 434 blocks until a connection is available. 435 436 @param databasename database from the dblist property of app.conf 437 @throws IllegalArgumentException if the specified database is not found 438 */ 439 public Connection getConnection(String databasename) throws SQLException 440 { 441 return getConnectionMgr(databasename).getConnection(); 442 } 443 444 /* 445 returns a connection from the connection pool to the default database. 446 blocks until a connection is available. 447 448 */ 449 public Connection getConnection() throws SQLException 450 { 451 if (defaultConnectionManager == null) { 452 throw new SQLException("The ConnectionManager in " + WebApp.class + " does not have \"default\" DB. Specify a DB name to get a connection to that DB."); 453 } 454 return defaultConnectionManager.getConnection(); 455 } 456 457 /* 458 Returns the default application log. 459 */ 460 public Log getAppLog() 461 { 462 return appLog; 463 } 464 465 /** 466 Convenience method to return a form stored previously via the {@link 467 putForm} method. Returns <tt>null</tt> if no form with the specified name 468 was found. 469 */ 470 public Form getForm(String name) 471 { 472 return (Form) appMap.get(name); 473 } 474 475 public void putForm(Form f) 476 { 477 appMap.put(f.getName(), f); 478 } 479 480 public void removeForm(String name) 481 { 482 if (appMap.containsKey(name)) 483 appMap.remove(name); 484 } 485 486 487 /** 488 Convenience method to get the pre-created application cache. This 489 can be used for caching database results as necessary. Each entry 490 in the cache can be stored for entry-specific time-to-live (see {@link Cache) 491 but if no entry-specific-time-to-live is specified, then entries are 492 cached for a default time of 5 minutes. Of course, cached entries should 493 always be invalidated sooner whenever the database is modified. 494 */ 495 public Cache getDBCache() 496 { 497 Object obj = appMap.get("_dbcache"); 498 499 if (obj != null) 500 return (Cache) obj; 501 502 Cache cache = null; 503 504 synchronized (WebApp.class) { //2 or more threads could be running in a page 505 cache = new MemoryCache( 506 Log.get("fc.util.cache"), "_dbcache", cache_time); 507 appMap.put("_dbcache", cache); 508 } 509 510 return cache; 511 } 512 513 /** 514 Returns the {@link ThreadLocalDateFormat} object corresponding to the specified 515 name (a new ThreadLocalDateFormat is created if it does not exist). 516 */ 517 public ThreadLocalDateFormat getThreadLocalDateFormat(String name) 518 { 519 ThreadLocalDateFormat tldf = (ThreadLocalDateFormat) tldfMap.get(name); 520 521 if (tldf == null) { 522 tldf = new ThreadLocalDateFormat(); 523 tldfMap.put(name, tldf); 524 } 525 526 return tldf; 527 } 528 529 /** 530 Returns the default threadLocalDateFormat object (a new 531 ThreadLocalDateFormat is created if it does not exist). 532 */ 533 public ThreadLocalDateFormat getThreadLocalDateFormat() 534 { 535 if (default_tldf == null) { 536 default_tldf = new ThreadLocalDateFormat(); 537 } 538 539 return default_tldf; 540 } 541 542 543 544 /** 545 Returns the ThreadLocalNumberFormat object corresponding to the specified 546 name (a new ThreadLocalNumberFormat is created if it does not exist). 547 */ 548 public ThreadLocalNumberFormat getThreadLocalNumberFormat(String name) 549 { 550 ThreadLocalNumberFormat tlnf = (ThreadLocalNumberFormat) tlnfMap.get(name); 551 552 if (tlnf == null) { 553 tlnf = new ThreadLocalNumberFormat(); 554 tlnfMap.put(name, tlnf); 555 } 556 557 return tlnf; 558 } 559 560 /** 561 Returns the default {@link ThreadLocalNumberFormat} object (a new 562 ThreadLocalNumberFormat is created if it does not exist). 563 */ 564 public ThreadLocalNumberFormat getThreadLocalNumberFormat() 565 { 566 if (default_tlnf == null) { 567 default_tlnf = new ThreadLocalNumberFormat(); 568 } 569 570 return default_tlnf; 571 } 572 573 /** 574 Returns the {@link ThreadLocalCalendar} object corresponding to the 575 specified name (a new ThreadLocalCalendar is created if it does 576 not exist). 577 */ 578 public ThreadLocalCalendar getThreadLocalCalendar(String name) 579 { 580 ThreadLocalCalendar tlcal = (ThreadLocalCalendar) tlcalMap.get(name); 581 582 if (tlcal == null) { 583 tlcal = new ThreadLocalCalendar(); 584 tlcalMap.put(name, tlcal); 585 } 586 587 return tlcal; 588 } 589 590 /** 591 Returns the default ThreadLocalCalendar object (a new ThreadLocalCalendar 592 is created if it does not exist). 593 */ 594 public ThreadLocalCalendar getThreadLocalCalendar() 595 { 596 if (default_tlcal == null) { 597 default_tlcal = new ThreadLocalCalendar(); 598 } 599 600 return default_tlcal; 601 } 602 603 /** 604 Returns the {@link ThreadLocalRandom} object corresponding to the 605 specified name (a new ThreadLocalRandom is created if it does 606 not exist). 607 */ 608 public ThreadLocalRandom getThreadLocalRandom(String name) 609 { 610 ThreadLocalRandom tlrand = (ThreadLocalRandom) tlrandMap.get(name); 611 612 if (tlrand == null) { 613 tlrand = new ThreadLocalRandom(); 614 tlrandMap.put(name, tlrand); 615 } 616 617 return tlrand; 618 } 619 620 /** 621 Returns the default ThreadLocalRandom object (a new ThreadLocalRandom 622 is created if it does not exist). 623 */ 624 public ThreadLocalRandom getThreadLocalRandom() 625 { 626 if (default_tlrand == null) { 627 default_tlrand = new ThreadLocalRandom(); 628 } 629 630 return default_tlrand; 631 } 632 633 /** 634 Returns the {@link ThreadLocalObject} corresponding to the 635 specified name (a new ThreadLocalObject is created if it does 636 not exist). 637 */ 638 public ThreadLocalObject getThreadLocalObject(String name) 639 { 640 ThreadLocalObject tlo = (ThreadLocalObject) tlMap.get(name); 641 642 if (tlo == null) { 643 tlo = new ThreadLocalObject(); 644 tlMap.put(name, tlo); 645 } 646 647 return tlo; 648 } 649 650 651 /** 652 Returns the specified object from the global application map or 653 <tt>null</tt> if the object was not found. 654 */ 655 public Object get(Object key) 656 { 657 return appMap.get(key); 658 } 659 660 /** 661 Puts the specified key/object into the global application map. 662 */ 663 public void put(Object key, Object val) 664 { 665 appMap.put(key, val); 666 } 667 668 public String toString() { 669 return new ToString(this).reflect().render(); 670 } 671 }