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 }