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&lt;context-param&gt;
035        &lt;param-name&gt;configfile&lt;/param-name&gt;
036        &lt;param-value&gt;app.conf&lt;/param-value&gt;
037&lt;/context-param&gt;
038&lt;context-param&gt;
039          &lt;param-name&gt;appName&lt;/param-name&gt;
040          &lt;param-value&gt;some-arbitrary-string-unique-across-<b>all</b>-webapps&lt;/param-value&gt;
041&lt;/context-param&gt;
042
043&lt;listener&gt;
044        &lt;listener-class&gt;fc.web.servlet.WebApp&lt;/listener-class&gt;
045&lt;/listener&gt;
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}