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.servlet; 007 008import javax.servlet.*; 009import javax.servlet.http.*; 010import java.io.*; 011import java.util.*; 012import java.sql.*; 013 014import fc.io.*; 015import fc.jdbc.*; 016import fc.util.*; 017import fc.util.cache.*; 018import fc.web.*; 019import fc.web.forms.*; 020 021/** 022Application level global object running within a servlet web application 023(i.e., a servlet context). Global webapp data can be stored/retrieved 024via the put/get methods. 025<p> 026Initializes and stores various variables useful for all servlets/pages 027running in our JVM. Implements {@link javax.servlet.ServletContextListener} 028and initializes itself when informed by the servlet container's context initialization event. 029<p> 030It's optional to use this class. If it is used, it's configured by adding the 031following 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 051shut down the entire servlet JVM</font> by calling <tt>System.exit</tt>. (The 052idea being it's better to fail early and safely then continue beyond this 053point). 054<p> 055If used, this class requires the following context configuration 056parameter: 057<blockquote> 058<ul> 059<li><tt>configfile</tt>: Path/name of the application configuration 060file. If the path starts with a '/', it is an absolute file system path. 061Otherwise, 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 063webapp</li> 064</ul> 065</blockquote> 066<p> 067This class can also be subclassed to initialize/contain website specific data 068and background/helper processing threads. Alternatively, along with this class 069as-is, additional independent site-specific ServletContextListener classes can 070be created and used as necessary. 071 072@author hursh jain 073*/ 074public class WebApp implements ServletContextListener 075{ 076//IMPL NOTE: synchronize stuff that is set here with AdminServlet 077/* 078Contains all the {@link fc.dbo.ConnectionMgr ConnectionManagers} 079for this application. 080*/ 081public Map connectionManagers = new HashMap(); 082public ConnectionMgr defaultConnectionManager; 083public long default_dbcache_time = MemoryCache.TWO_MIN; 084public ThreadLocalCalendar default_tlcal; 085public ThreadLocalDateFormat default_tldf; 086public ThreadLocalNumberFormat default_tlnf; 087public ThreadLocalRandom default_tlrand; 088public Map appMap = new Hashtable(); //hashtable is sync'ed 089public Map tlcalMap = new Hashtable(); //ht is sync'ed 090public Map tldfMap = new Hashtable(); //ht is sync'ed 091public Map tlnfMap = new Hashtable(); //ht is sync'ed 092public Map tlrandMap = new Hashtable(); //ht is sync'ed 093public Map tlMap = new Hashtable(); //ht is sync'ed 094 095/** 096A {@link Log} object. Servlets typically create their own 097loggers (with servlet specific logging levels) but can alternatively 098use this default appLog. This appLog is used by non-servlet classes 099such as this class itself, various listeners etc. 100*/ 101public Log appLog; 102public PropertyMgr propertyMgr; 103 long cache_time; //in ms 104 105/** The required 'appName' parameter read from the config file (web.xml) **/ 106protected String appName; 107 108/** 109A (initially empty) Map that servlets can use to store a reference to 110themselves. This is required because the servlet API has deprecated a 111similar API call (the servlet API authors are brain damaged 'tards). 112*/ 113public Map allServletsMap = new HashMap(); 114private static Map<String, WebApp> instances = new HashMap(); 115/** this is for WebApps added externally/manually by a servlet via addWebApp. When 116the context/webapp under which that servlet is running is destroyed, that manually 117added webapp is also automatically destroyed */ 118private static Set autoDestroySet = new HashSet(); 119 120 121/** 122A no-arg constructor public such that the servlet container can instantiate this class. 123*/ 124public WebApp() { } 125 126/* 127Returns an instance of the webapp configured for this application (or <tt>null</tt> 128if not yet configured (via the servlet context initialization) or not found. 129*/ 130public static WebApp getInstance(String appName) 131 { 132 synchronized (WebApp.class) 133 { 134 return instances.get(appName); 135 } 136 } 137 138/** 139Basic implementation of the web application cleanup upon context creation. 140 141If this method is subclassed, the subclassed method must also invoke this 142method via <tt>super.contextInitialized</tt>. This call should typically 143be at the beginning of the subclassed method, which allows this implementation 144to create connections, logs etc, which can then be used by the subclass to 145finish it's further initialization as needed. 146*/ 147public 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/** 176Basic implementation of the web application cleanup upon context destruction. 177 178If this method is subclassed, the subclassed method must also invoke this 179method via <tt>super.contextInitialized</tt>. This invokation should typically 180be at the <b>end</b> of the subclassed method (which allows the subclass to 181use connections etc., before they are closed by this superclass method). 182*/ 183public 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 211private 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/** 229Add a new WebApp instance manually. A WebApp is typically specified as a context 230listener, in a particular <i>web.xml</i>, and configures itself as representing 231that context wide configuration (common to all servlets and pages running inside 232that context). There is only 1 instance of the WebApp class that is instantiated 233per context by the servlet container. 234<p> 235However, for REST services and other API's, it is useful to have several API 236versions, such as <tt>/rest/v1/</tt>, <tt>/rest/v2/</tt>, etc., all of which are 237tied to seperate servlets in the <b>same</b> webapp. We could also create 238separate 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 240separate configuration). 241<p> 242However, we sometimes need to access a particular REST api's <b>classes</b> directly 243from the "root" document webapp (which is running molly pages, etc). If the REST api's 244are in separate contexts, the root webapp (a different webapp from all the other 245REST api versioned webapps) cannot access those api classes directly. This becomes 246a hassle if we want to use our API directly to show results on a molly web page. 247<p> 248So 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 250version of the API. 251<p> 252So then, each of these servlets have to be configured as well with connection pools, 253loggers, etc, each of them specific to a particular servlet/api. This can be done 254via servlet init parameters, but since the whole point of WebApp is to set up this 255configuration easily, it is also useful to create a WebApp instance <i>per 256servlet instance</i> in the same webapp. 257<p> 258Servlets can then call this method with different appNames (unique to each servlet) 259and different configuration files (again unique to each servlet). When the context 260is destroyed, all these manually added webapps will also be automatically closed. 261<p> 262This class must be specified as a listener in web.xml (even if the context wide 263config file has no configuration data, use an empty config file in that case). 264<p> 265PS: 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*/ 271public 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 280private 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/** 399Returns the property manager associated with this WebApp (properties of the app 400configuration file can be read via this property mgr) 401*/ 402public PropertyMgr getPropertyMgr() 403 { 404 return propertyMgr; 405 } 406 407/** 408Returns the connection manager corresponding to the database 409name. (specified in the dblist property of app.conf). 410 411@throws IllegalArgumentException if the specified database is not found 412*/ 413public 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/** 423Returns the connection manager corresponding to the default database 424name. (specified in the dbdefault property of app.conf). Returns <tt>null</tt> 425if no default database has been initialized. 426*/ 427public ConnectionMgr getConnectionMgr() 428 { 429 return defaultConnectionManager; 430 } 431 432/* 433returns a connection from the connection pool to the database specified. 434blocks 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*/ 439public Connection getConnection(String databasename) throws SQLException 440 { 441 return getConnectionMgr(databasename).getConnection(); 442 } 443 444/* 445returns a connection from the connection pool to the default database. 446blocks until a connection is available. 447 448*/ 449public 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/* 458Returns the default application log. 459*/ 460public Log getAppLog() 461 { 462 return appLog; 463 } 464 465/** 466Convenience method to return a form stored previously via the {@link 467putForm} method. Returns <tt>null</tt> if no form with the specified name 468was found. 469*/ 470public Form getForm(String name) 471 { 472 return (Form) appMap.get(name); 473 } 474 475public void putForm(Form f) 476 { 477 appMap.put(f.getName(), f); 478 } 479 480public void removeForm(String name) 481 { 482 if (appMap.containsKey(name)) 483 appMap.remove(name); 484 } 485 486 487/** 488Convenience method to get the pre-created application cache. This 489can be used for caching database results as necessary. Each entry 490in the cache can be stored for entry-specific time-to-live (see {@link Cache) 491but if no entry-specific-time-to-live is specified, then entries are 492cached for a default time of 5 minutes. Of course, cached entries should 493always be invalidated sooner whenever the database is modified. 494*/ 495public 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/** 514Returns the {@link ThreadLocalDateFormat} object corresponding to the specified 515name (a new ThreadLocalDateFormat is created if it does not exist). 516*/ 517public 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/** 530Returns the default threadLocalDateFormat object (a new 531ThreadLocalDateFormat is created if it does not exist). 532*/ 533public ThreadLocalDateFormat getThreadLocalDateFormat() 534 { 535 if (default_tldf == null) { 536 default_tldf = new ThreadLocalDateFormat(); 537 } 538 539 return default_tldf; 540 } 541 542 543 544/** 545Returns the ThreadLocalNumberFormat object corresponding to the specified 546name (a new ThreadLocalNumberFormat is created if it does not exist). 547*/ 548public 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/** 561Returns the default {@link ThreadLocalNumberFormat} object (a new 562ThreadLocalNumberFormat is created if it does not exist). 563*/ 564public ThreadLocalNumberFormat getThreadLocalNumberFormat() 565 { 566 if (default_tlnf == null) { 567 default_tlnf = new ThreadLocalNumberFormat(); 568 } 569 570 return default_tlnf; 571 } 572 573/** 574Returns the {@link ThreadLocalCalendar} object corresponding to the 575specified name (a new ThreadLocalCalendar is created if it does 576not exist). 577*/ 578public 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/** 591Returns the default ThreadLocalCalendar object (a new ThreadLocalCalendar 592is created if it does not exist). 593*/ 594public ThreadLocalCalendar getThreadLocalCalendar() 595 { 596 if (default_tlcal == null) { 597 default_tlcal = new ThreadLocalCalendar(); 598 } 599 600 return default_tlcal; 601 } 602 603/** 604Returns the {@link ThreadLocalRandom} object corresponding to the 605specified name (a new ThreadLocalRandom is created if it does 606not exist). 607*/ 608public 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/** 621Returns the default ThreadLocalRandom object (a new ThreadLocalRandom 622is created if it does not exist). 623*/ 624public ThreadLocalRandom getThreadLocalRandom() 625 { 626 if (default_tlrand == null) { 627 default_tlrand = new ThreadLocalRandom(); 628 } 629 630 return default_tlrand; 631 } 632 633/** 634Returns the {@link ThreadLocalObject} corresponding to the 635specified name (a new ThreadLocalObject is created if it does 636not exist). 637*/ 638public 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/** 652Returns the specified object from the global application map or 653<tt>null</tt> if the object was not found. 654*/ 655public Object get(Object key) 656 { 657 return appMap.get(key); 658 } 659 660/** 661Puts the specified key/object into the global application map. 662*/ 663public void put(Object key, Object val) 664 { 665 appMap.put(key, val); 666 } 667 668public String toString() { 669 return new ToString(this).reflect().render(); 670 } 671}