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