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    &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
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    }