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    &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    
039    &lt;listener&gt;
040            &lt;listener-class&gt;fc.web.servlet.WebApp&lt;/listener-class&gt;
041    &lt;/listener&gt;
042    </pre>
043    </blockquote>
044    <p>
045    <b>Important</b>:<b> If</b> this class is used <i>and</i> its initialization
046    is not successful, it tries to shut down the servlet JVM by calling
047    <tt>System.exit</tt>. (The idea being it's better to fail early and safely then
048    continue beyond this point). 
049    <p>
050    If used, this class requires the following context configuration 
051    parameter:
052    <blockquote>
053    <ul>
054    <li><tt>configfile</tt>: Path/name of the application configuration
055    file. If the path starts with a '/', it is an absolute file system path.
056    Otherwise, it is relative to this context root's WEB-INF directory.</li>
057    </ul>
058    </blockquote>
059    <p>
060    This class can also be subclassed to initialize/contain website specific data
061    and background/helper processing threads. Alternatively, along with this class
062    as-is, additional independent site-specific ServletContextListener classes can
063    be created and used as necessary.
064    
065    @author hursh jain
066    */
067    public class WebApp implements ServletContextListener
068    {
069    //IMPL NOTE: synchronize stuff that is set here with AdminServlet
070    /*
071    Contains all the {@link fc.dbo.ConnectionMgr ConnectionManagers} 
072    for this application.
073    */
074    public static Map           connectionManagers = new HashMap();
075    public static ConnectionMgr       defaultConnectionManager;
076    public static long            default_dbcache_time = MemoryCache.TWO_MIN;
077    public static ThreadLocalCalendar   default_tlcal;
078    public static ThreadLocalDateFormat   default_tldf;
079    public static ThreadLocalNumberFormat default_tlnf;
080    public static ThreadLocalRandom     default_tlrand;
081    public static Map           appMap   = new Hashtable(); //hashtable is sync'ed
082    public static Map           tlcalMap = new Hashtable(); //ht is sync'ed
083    public static Map           tldfMap  = new Hashtable(); //ht is sync'ed
084    public static Map           tlnfMap  = new Hashtable(); //ht is sync'ed
085    public static Map           tlrandMap = new Hashtable(); //ht is sync'ed
086    
087    /**
088    A {@link SystemLog} object. Servlets typically create their own
089    loggers (with servlet specific logging levels) but can alternatively
090    use this default appLog. This appLog is used by non-servlet classes
091    such as this class itself, various listeners etc.
092    */
093    public static SystemLog   appLog = Log.getDefault();
094    public static PropertyMgr   propertyMgr;
095         static long      cache_time; //in ms
096    
097    /** 
098    A (initially empty) Map that servlets can use to store a reference to
099    themselves. This is required because the servlet API has deprecated a
100    similar API call (the servlet API authors are brain damaged 'tards).
101    */
102    public static Map   allServletsMap = new HashMap();
103    
104    /**
105    Basic implementation of the web application cleanup upon context creation.
106    
107    If this method is subclassed, the subclassed method must also invoke this
108    method via <tt>super.contextInitialized</tt>. This call should typically
109    be at the beginning of the subclassed method, which allows this implementation
110    to create connections, logs etc, which can then be used by the subclass to
111    finish it's further initialization as needed.
112    */
113    public void contextInitialized(ServletContextEvent sce)
114      {
115      ServletContext context = sce.getServletContext();
116      
117      java.util.Date now = Calendar.getInstance().getTime();
118      System.out.println("fc.web.servlet.WebApp: starting initialization on: " + now);
119    
120      try {
121        String sconf = WebUtil.getRequiredParam(context, "configfile");
122        
123        File conf = new File(sconf);
124        
125        //We print to system.out to ensure we see this regardless of loglevel.
126        //That's because this is very useful information in case of problems
127        //starting the webapp.
128        System.out.println("-> WebApp: WEB-INF directory: " + context.getRealPath("/WEB-INF"));
129        System.out.println("-> WebApp: Configuration file: " + conf.getPath());
130        
131        if (! conf.isAbsolute()) {
132          conf = new File(context.getRealPath("/WEB-INF"), conf.getPath());
133          }
134          
135          propertyMgr = new FilePropertyMgr(conf);
136    
137        String level = WebApp.propertyMgr.get("log.level", null);
138        if (level != null)  {
139          appLog.setLevel(level);
140          Log.setDefaultLevel(level);
141          }
142          
143        ConnectionMgr defaultCmgr = null;
144    
145        String dbdefault_str = propertyMgr.get("db.default");
146        appLog.info("WebApp: Default database = ", 
147            dbdefault_str == null ? "Not specified" : dbdefault_str);
148        
149        String dblist_str = propertyMgr.get("db.list");
150        appLog.info("WebApp: All databases: ", dblist_str == null ?
151                        "None specified" : dblist_str);
152        
153        if (dblist_str == null) {
154          if (dbdefault_str != null) {
155            throw new IllegalArgumentException("Since a default database was specified, a dblist must also be specified (but was null)");
156            }
157          }
158        else{ 
159          String[] cmgrnames = dblist_str.split(",");   
160          for (String dbname : cmgrnames) 
161            {
162            dbname = dbname.trim();
163            String poolsize = propertyMgr.get(dbname + ".pool.size");    
164            String jdbc_url = propertyMgr.get(dbname +  ".jdbc.url");
165            String jdbc_driver = propertyMgr.get(dbname +  ".jdbc.driver");
166            String jdbc_user = propertyMgr.get(dbname +  ".jdbc.user");
167            String jdbc_password = propertyMgr.get(dbname +  ".jdbc.password");
168            String jdbc_catalog = propertyMgr.get(dbname +  ".jdbc.catalog", "");
169          
170            ConnectionMgr cmgr = new PooledConnectionMgr(
171                jdbc_url, jdbc_driver, jdbc_user, jdbc_password, 
172                jdbc_catalog, Integer.parseInt(poolsize));
173          
174            connectionManagers.put(dbname, cmgr);
175            if (dbname.equalsIgnoreCase(dbdefault_str))
176              defaultConnectionManager = cmgr;
177            }
178          }
179        
180        String cache_time_str = propertyMgr.get("db.cache_time_seconds");
181      
182        if (cache_time_str != null)
183          cache_time = Long.parseLong(cache_time_str) * 1000;
184        else
185          cache_time = default_dbcache_time;
186      
187        appLog.info("WebApp: db.cache_time = ", cache_time/1000, " seconds"); 
188      
189        QueryUtil.init(appLog);
190        
191        appLog.info("===> WebApp finished [success]");
192        }
193      catch (Exception e) 
194        {
195        System.err.println("**** ERROR: exception in " + getClass().getName() + " *****");
196        System.err.println("**** Shutting down the Web Server ****");
197        System.err.flush();
198        e.printStackTrace(System.err); //send email to someone here ?
199        System.exit(1);
200        }
201      } //~WebAppialized
202    
203    
204    /**
205    Basic implementation of the web application cleanup upon context destruction.
206    
207    If this method is subclassed, the subclassed method must also invoke this
208    method via <tt>super.contextInitialized</tt>. This invokation should typically
209    be at the <b>end</b> of the subclassed method (which allows the subclass to
210    use connections etc., before they are closed by this superclass method).
211    */
212    public void contextDestroyed(ServletContextEvent sce)
213      {
214      appLog.info("WebApp closing at: " + new java.util.Date());
215      Iterator it = connectionManagers.values().iterator();
216      
217      while (it.hasNext()) {
218        ConnectionMgr cmgr = (ConnectionMgr) it.next();
219        cmgr.close();
220        }
221      }
222            
223    /** 
224    Returns the connection manager corresponding to the database
225    name. (specified in the dblist property of app.conf). 
226    
227    @throws IllegalArgumentException  if the specified database is not found
228    */
229    public static ConnectionMgr getConnectionMgr(String databasename)
230      {
231      ConnectionMgr cm = (ConnectionMgr) connectionManagers.get(databasename);
232      if (cm == null) 
233        throw new IllegalArgumentException("The specified connectionManager [" + databasename + "] does not exist or has not been initialized.");
234      return cm;  
235      }
236      
237    /** 
238    Returns the connection manager corresponding to the default database
239    name. (specified in the dbdefault property of app.conf). Returns <tt>null</tt>
240    if no default database has been initialized.
241    */
242    public static ConnectionMgr getConnectionMgr()
243      {
244      return defaultConnectionManager;
245      }
246    
247    /*
248    returns a connection from the connection pool to the database specified.
249    blocks until a connection is available.
250    
251    @param databasename  database from the dblist property of app.conf
252    @throws IllegalArgumentException  if the specified database is not found
253    */
254    public static Connection getConnection(String databasename) throws SQLException
255      {
256      return getConnectionMgr(databasename).getConnection();
257      }
258      
259    /*
260    returns a connection from the connection pool to the default database.
261    blocks until a connection is available.
262    
263    */
264    public static Connection getConnection() throws SQLException
265      {
266      if (defaultConnectionManager == null) {
267        throw new SQLException("The ConnectionManager in " + WebApp.class + " has not been intialized properly. So how can I return a connection ?");
268        }
269      return defaultConnectionManager.getConnection();
270      }
271      
272    /*
273    Returns the default application log.
274    */
275    public static SystemLog getAppLog()
276      {
277      return appLog;
278      }
279    
280    /**
281    Convenience method to return a form stored previously via the {@link
282    putForm} method. Returns <tt>null</tt> if no form with the specified name
283    was found.
284    */
285    public static Form getForm(String name)
286      {
287      return (Form) appMap.get(name);
288      }
289    
290    public static void putForm(Form f)
291      {
292      appMap.put(f.getName(), f);
293      }
294      
295    public static void removeForm(String name)
296      {
297      if (appMap.containsKey(name))
298        appMap.remove(name);
299      }
300    
301    
302    /**
303    Convenience method to get the pre-created application cache. This
304    can be used for caching database results as necessary. Each entry
305    in the cache can be stored for entry-specific time-to-live (see {@link Cache)
306    but if no entry-specific-time-to-live is specified, then entries are
307    cached for a default time of 5 minutes. Of course, cached entries should
308    always be invalidated sooner whenever the database is modified.
309    */
310    public static Cache getDBCache()
311      {
312      Object obj = appMap.get("_dbcache");
313      
314      if (obj != null) 
315        return (Cache) obj;
316      
317      Cache cache = null;
318      
319      synchronized (WebApp.class) {  //2 or more threads could be running in a page
320        cache = new MemoryCache(
321            Log.get("fc.util.cache"), "_dbcache", cache_time);
322        appMap.put("_dbcache", cache);
323        }
324        
325      return cache; 
326      }
327    
328    /** 
329    Returns the ThreadLocalDateFormat object corresponding to the specified
330    name (a new ThreadLocalDateFormat is created if it does not exist).
331    */
332    public static ThreadLocalDateFormat getThreadLocalDateFormat(String name)
333      {
334      ThreadLocalDateFormat tldf = (ThreadLocalDateFormat) tldfMap.get(name);
335      
336      if (tldf == null) {
337        tldf = new ThreadLocalDateFormat();
338        tldfMap.put(tldf, name);
339        }
340        
341      return tldf;
342      }
343      
344    /** 
345    Returns the default threadLocalDateFormat object (a new
346    ThreadLocalDateFormat is created if it does not exist).
347    */
348    public static ThreadLocalDateFormat getThreadLocalDateFormat()
349      {
350      if (default_tldf == null) {
351        default_tldf = new ThreadLocalDateFormat();
352        }
353        
354      return default_tldf;
355      }
356      
357    
358    
359    /** 
360    Returns the ThreadLocalNumberFormat object corresponding to the specified
361    name (a new ThreadLocalNumberFormat is created if it does not exist).
362    */
363    public static ThreadLocalNumberFormat getThreadLocalNumberFormat(String name)
364      {
365      ThreadLocalNumberFormat tlnf = (ThreadLocalNumberFormat) tlnfMap.get(name);
366      
367      if (tlnf == null) {
368        tlnf = new ThreadLocalNumberFormat();
369        tlnfMap.put(tlnf, name);
370        }
371        
372      return tlnf;
373      }
374      
375    /** 
376    Returns the default ThreadLocalNumberFormat object (a new
377    ThreadLocalNumberFormat is created if it does not exist).
378    */
379    public static ThreadLocalNumberFormat getThreadLocalNumberFormat()
380      {
381      if (default_tlnf == null) {
382        default_tlnf = new ThreadLocalNumberFormat();
383        }
384        
385      return default_tlnf;
386      }
387      
388    /** 
389    Returns the ThreadLocalCalendar object corresponding to the
390    specified name (a new ThreadLocalCalendar is created if it does
391    not exist).
392    */
393    public static ThreadLocalCalendar getThreadLocalCalendar(String name)
394      {
395      ThreadLocalCalendar tlcal = (ThreadLocalCalendar) tlcalMap.get(name);
396      
397      if (tlcal == null) {
398        tlcal = new ThreadLocalCalendar();
399        tlcalMap.put(tlcal, name);
400        }
401        
402      return tlcal;
403      }
404      
405    /** 
406    Returns the default ThreadLocalCalendar object (a new ThreadLocalCalendar
407    is created if it does not exist).
408    */
409    public static ThreadLocalCalendar getThreadLocalCalendar()
410      {
411      if (default_tlcal == null) {
412        default_tlcal = new ThreadLocalCalendar();
413        }
414        
415      return default_tlcal;
416      }
417    
418    /** 
419    Returns the ThreadLocalRandom object corresponding to the
420    specified name (a new ThreadLocalRandom is created if it does
421    not exist).
422    */
423    public static ThreadLocalRandom getThreadLocalRandom(String name)
424      {
425      ThreadLocalRandom tlrand = (ThreadLocalRandom) tlrandMap.get(name);
426      
427      if (tlrand == null) {
428        tlrand = new ThreadLocalRandom();
429        tlrandMap.put(tlrand, name);
430        }
431        
432      return tlrand;
433      }
434      
435    /** 
436    Returns the default ThreadLocalRandom object (a new ThreadLocalRandom
437    is created if it does not exist).
438    */
439    public static ThreadLocalRandom getThreadLocalRandom()
440      {
441      if (default_tlrand == null) {
442        default_tlrand = new ThreadLocalRandom();
443        }
444        
445      return default_tlrand;
446      }
447      
448    /**
449    Returns the specified object from the global application map or 
450    <tt>null</tt> if the object was not found.
451    */  
452    public static Object get(Object key)
453      {
454      return appMap.get(key);
455      }
456    
457    /**
458    Puts the specified key/object into the global application map.
459    */  
460    public static void put(Object key, Object val)
461      {
462      appMap.put(key, val);
463      }
464    
465    public String toString() {
466      return new ToString(this).reflect().render();
467      }
468    }