1    /*
2     * Copyright 2005 :torweg free software group
3     * 
4     * This program is free software: you can redistribute it and/or modify
5     * it under the terms of the GNU General Public License as published by
6     * the Free Software Foundation, either version 3 of the License, or
7     * (at your option) any later version.
8     * 
9     * This program is distributed in the hope that it will be useful,
10    * but WITHOUT ANY WARRANTY; without even the implied warranty of
11    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    * GNU General Public License for more details.
13    * 
14    * You should have received a copy of the GNU General Public License
15    * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16    * 
17    */
18   package org.torweg.pulse.invocation.lifecycle;
19   
20   import java.io.File;
21   import java.io.UnsupportedEncodingException;
22   import java.security.NoSuchAlgorithmException;
23   import java.security.SecureRandom;
24   import java.util.ArrayList;
25   import java.util.Arrays;
26   import java.util.Collection;
27   import java.util.HashSet;
28   import java.util.List;
29   import java.util.Random;
30   import java.util.Set;
31   import java.util.Timer;
32   import java.util.TimerTask;
33   import java.util.concurrent.ConcurrentHashMap;
34   
35   import javax.xml.bind.JAXBContext;
36   import javax.xml.bind.JAXBException;
37   import javax.xml.bind.Unmarshaller;
38   
39   import org.apache.fop.apps.FopFactory;
40   import org.apache.log4j.NDC;
41   import org.slf4j.Logger;
42   import org.slf4j.LoggerFactory;
43   import org.torweg.pulse.accesscontrol.attributes.AttributeFactory;
44   import org.torweg.pulse.annotations.Action.Security;
45   import org.torweg.pulse.bundle.Bundle;
46   import org.torweg.pulse.bundle.JobletScheduler;
47   import org.torweg.pulse.component.Component;
48   import org.torweg.pulse.configuration.Configuration;
49   import org.torweg.pulse.configuration.ConfigurationException;
50   import org.torweg.pulse.configuration.PoorMansCache;
51   import org.torweg.pulse.email.MailQueue;
52   import org.torweg.pulse.email.MailQueueConfiguration;
53   import org.torweg.pulse.service.ServletConfig;
54   import org.torweg.pulse.service.request.Locale;
55   import org.torweg.pulse.service.request.LocaleManager;
56   import org.torweg.pulse.util.HibernateDataSource;
57   import org.torweg.pulse.util.HibernateDataSourceImpl;
58   import org.torweg.pulse.util.captcha.ICaptchaAdapter;
59   import org.torweg.pulse.util.geolocation.IGeoLocationProvider;
60   import org.torweg.pulse.util.geolocation.NoLookupLocationProvider;
61   import org.torweg.pulse.vfs.VirtualFileSystem;
62   
63   import com.sun.jersey.api.json.JSONJAXBContext;
64   
65   /**
66    * initialises all bundles and manages the life-cycle of reloadable components.
67    * <p>
68    * The {@code Lifecycle} singleton initialises all bundles of the <em>pulse</em>
69    * container and the config pool. If the container is configured for reloading
70    * the {@code Lifecycle} singleton watches registered resources for changes and
71    * restarts the affected sub-systems on demand.
72    * </p>
73    * <p>
74    * Moreover it globally initialises the {@code HibernateDataSource}, all {@code
75    * Bundle}s and their resources as well as the {@code ContentRegistry} and
76    * {@code Sitemap}.
77    * </p>
78    * <p>
79    * The {@code Lifecycle} sets up a global JAXB context, registers the {@code
80    * IP2CountryProvider} and sets up the {@code MailQueue} as well as the {@code
81    * VirtualFileSystem}.
82    * </p>
83    * 
84    * @author Thomas Weber, Christian Schatt
85    * @version $Revision: 1932 $
86    * @see PoorMansCache
87    * @see Bundle
88    * @see org.torweg.pulse.util.HibernateDataSource
89    * @see org.torweg.pulse.site.content.ContentRegistry
90    */
91   public final class Lifecycle {
92   
93       /**
94        * the logger.
95        */
96       private static final Logger LOGGER = LoggerFactory
97               .getLogger(Lifecycle.class);
98   
99       /**
100       * the singleton itself.
101       */
102      private static Lifecycle lifecycleInstance;
103  
104      /**
105       * the default random source (usually a {@code SecureRandom}).
106       */
107      private static Random random;
108  
109      /**
110       * the root directory of the <em>pulse</em> webapp.
111       */
112      private final File pulseRootDir;
113  
114      /**
115       * the config directory for the <em>pulse</em> core.
116       */
117      private final File coreConfigDir;
118  
119      /**
120       * the root directory for all bundles.
121       */
122      private final File bundlesRootDir;
123  
124      /**
125       * the bundles.
126       */
127      private final ConcurrentHashMap<String, Bundle> bundles = new ConcurrentHashMap<String, Bundle>();
128  
129      /**
130       * the components.
131       */
132      private final Set<Component> components = new HashSet<Component>();
133  
134      /**
135       * the Timer for the WatchDog.
136       */
137      private Timer timer;
138  
139      /**
140       * The HibernateDataSource for this Lifecycle.
141       */
142      private HibernateDataSourceImpl hibernateDataSource;
143  
144      /**
145       * the locales known to the installation.
146       */
147      private Collection<Locale> locales;
148  
149      /**
150       * the global {@code JAXBContext}.
151       */
152      private JAXBContext jaxbContext;
153  
154      /**
155       * the {@code MailQueue}.
156       */
157      private MailQueue mailQueue;
158  
159      /**
160       * the ip to country lookup provider.
161       */
162      private IGeoLocationProvider geoLocationProvider;
163  
164      /**
165       * The {@code ICaptchaAdapter} if configured, {@code null} otherwise.
166       */
167      private ICaptchaAdapter<?> captchaAdapter;
168  
169      /**
170       * the attribute factory.
171       */
172      private AttributeFactory attributeFactory;
173  
174      /**
175       * the watch dog.
176       */
177      private WatchDog watchDog;
178  
179      /**
180       * The {@code FopFactory}-instance.
181       */
182      private FopFactory fopFactoryInstance;
183  
184      /**
185       * the JSON JAXB context.
186       */
187      private JSONJAXBContext jsonJaxbContext;
188  
189      /**
190       * the salt to be used for creating salted hashes.
191       * 
192       * @see Lifecycle#getSaltedHash(byte[])
193       */
194      private byte[] serverSalt;
195  
196      /**
197       * private constructor for the singleton.
198       * 
199       * @param pulseWebapp
200       *            the root directory of the <em>pulse</em> webapp
201       */
202      private Lifecycle(final File pulseWebapp) {
203          this.pulseRootDir = pulseWebapp;
204          this.coreConfigDir = new File(this.pulseRootDir, "WEB-INF"
205                  + File.separator + "conf");
206          this.bundlesRootDir = new File(pulseWebapp, "WEB-INF" + File.separator
207                  + "bundles");
208      }
209  
210      /**
211       * returns the {@code AttributeFactory}.
212       * 
213       * @return the {@code AttributeFactory}
214       */
215      public static AttributeFactory getAttributeFactory() {
216          if (lifecycleInstance != null) {
217              return lifecycleInstance.attributeFactory;
218          }
219          throw new LifecycleException("The Lifecycle has not been started.");
220      }
221  
222      /**
223       * @return the bundles of the <em>pulse</em> container
224       */
225      public static Collection<Bundle> getBundles() {
226          if (lifecycleInstance != null) {
227              return lifecycleInstance.bundles.values();
228          }
229          throw new LifecycleException("The Lifecycle has not been started.");
230      }
231  
232      /**
233       * get the Bundle with the specified name.
234       * 
235       * @param name
236       *            the name of the bundle
237       * @return the Bundle or {@code null}, if no such Bundle exists.
238       */
239      public static Bundle getBundle(final String name) {
240          if (lifecycleInstance != null) {
241              return lifecycleInstance.bundles.get(name);
242          }
243          throw new LifecycleException("The Lifecycle has not been started.");
244      }
245  
246      /* --- START: protected direct access methods for Lifecycle tasks --------- */
247  
248      /**
249       * returns the containers root directory.
250       * 
251       * @return the containers root directory
252       */
253      public static File getBasePath() {
254          if (lifecycleInstance != null) {
255              return lifecycleInstance.pulseRootDir.getAbsoluteFile();
256          }
257          throw new LifecycleException("The Lifecycle has not been started.");
258      }
259  
260      /**
261       * Returns the {@code HibernateDataSource}.
262       * 
263       * @return the {@code HibernateDataSource}
264       */
265      public static HibernateDataSource getHibernateDataSource() {
266          if (lifecycleInstance != null) {
267              return lifecycleInstance.hibernateDataSource;
268          }
269          throw new LifecycleException("The Lifecycle has not been started.");
270      }
271  
272      /**
273       * returns the {@code IGeoLocationProvider}.
274       * 
275       * @return the {@code IGeoLocationProvider}
276       */
277      public static IGeoLocationProvider getGeoLocationProvider() {
278          if (lifecycleInstance != null) {
279              return lifecycleInstance.geoLocationProvider;
280          }
281          throw new LifecycleException("The Lifecycle has not been started.");
282      }
283  
284      /**
285       * Returns the (configured) {@code ICaptchaAdapter}.
286       * 
287       * @return the captcha-adapter
288       */
289      public static ICaptchaAdapter<?> getCaptchaAdapter() {
290          if (lifecycleInstance != null) {
291              return lifecycleInstance.captchaAdapter;
292          }
293          throw new LifecycleException("The Lifecycle has not been started.");
294      }
295  
296      /**
297       * Returns the {@code Lifecycle}s' (configured) {@code FopFactory}
298       * -instance.
299       * 
300       * @return the {@code Lifecycle}s' {@code FopFactory}-instance
301       */
302      public static FopFactory getFopFactory() {
303          if (lifecycleInstance != null) {
304              return lifecycleInstance.fopFactoryInstance;
305          }
306          throw new LifecycleException("The Lifecycle has not been started.");
307      }
308  
309      /**
310       * @see javax.xml.bind.JAXBContext
311       * @return the global JAXBContext
312       */
313      public static JAXBContext getJAXBContext() {
314          return lifecycleInstance.jaxbContext;
315      }
316  
317      /**
318       * 
319       * @return a {@code JSONJAXBContext#newInstance(String)} with a JSON mapping
320       *         of {@code JSONConfiguration#Notation#MAPPED}
321       */
322      public static JSONJAXBContext getJSONJAXBContext() {
323          return lifecycleInstance.jsonJaxbContext;
324      }
325  
326      /**
327       * returns a list of all known {@code {@link Locale}s}.
328       * 
329       * @return a list of all known {@code {@link Locale}s}
330       * @see #getActiveLocales()
331       */
332      public static Collection<java.util.Locale> getKnownLocales() {
333          if (lifecycleInstance != null) {
334              return convertLocaleList(lifecycleInstance.locales, false);
335          }
336          throw new LifecycleException("The Lifecycle has not been started.");
337      }
338  
339      /**
340       * returns a list of all active {@code {@link Locale}s}.
341       * 
342       * @return a list of all active {@code {@link Locale}s}
343       * @see #getKnownLocales()
344       */
345      public static Collection<java.util.Locale> getActiveLocales() {
346          if (lifecycleInstance != null) {
347              return convertLocaleList(lifecycleInstance.locales, true);
348          }
349          throw new LifecycleException("The Lifecycle has not been started.");
350      }
351  
352      /**
353       * returns the {@code MailQueue}.
354       * 
355       * @return the {@code MailQueue}
356       */
357      public static MailQueue getMailQueue() {
358          if (lifecycleInstance != null) {
359              return lifecycleInstance.mailQueue;
360          }
361          throw new LifecycleException("The Lifecycle has not been started.");
362      }
363  
364      /**
365       * indicates whether <em>pulse</em> is configured to use TLS.
366       * 
367       * @return {@code true}, if TLS is available
368       */
369      public static boolean isTransportLayerSecurityAvailable() {
370          if (lifecycleInstance != null) {
371              return lifecycleInstance.getPulseConfiguration()
372                      .isTransportLayerSecurityAvailable();
373          }
374          throw new LifecycleException("The Lifecycle has not been started.");
375      }
376  
377      /**
378       * returns the weakest security level {@link Security} to be honoured, if
379       * TLS is available.
380       * 
381       * @return the weakest security level to be honoured
382       * @see #isTransportLayerSecurityAvailable()
383       */
384      public static Security getWeakestCommandSecurityLevel() {
385          if (lifecycleInstance != null) {
386              return lifecycleInstance.getPulseConfiguration()
387                      .getWeakestCommandSecurityLevel();
388          }
389          throw new LifecycleException("The Lifecycle has not been started.");
390      }
391  
392      /**
393       * returns the port to be used for standard HTTP connections.
394       * 
395       * @return the port to be used for standard HTTP connections
396       */
397      public static int getDefaultPort() {
398          if (lifecycleInstance != null) {
399              return lifecycleInstance.getPulseConfiguration().getDefaultPort();
400          }
401          throw new LifecycleException("The Lifecycle has not been started.");
402      }
403  
404      /**
405       * returns the port to be used for secure (HTTPS) connections.
406       * 
407       * @return the port to be used for secure (HTTPS) connections
408       */
409      public static int getSecurePort() {
410          if (lifecycleInstance != null) {
411              return lifecycleInstance.getPulseConfiguration().getSecurePort();
412          }
413          throw new LifecycleException("The Lifecycle has not been started.");
414      }
415  
416      /**
417       * returns the versioning prefix (see:
418       * {@link org.torweg.pulse.service.VersionRewriteFilter}).
419       * 
420       * @return the versioning prefix
421       */
422      public static String getVersioningPrefix() {
423          if (lifecycleInstance != null) {
424              return lifecycleInstance.getPulseConfiguration()
425                      .getVersioningPrefix();
426          }
427          throw new LifecycleException("The Lifecycle has not been started.");
428      }
429  
430      /**
431       * returns the default random source which usually is a {@code SecureRandom}
432       * .
433       * 
434       * @return the default random source
435       */
436      public static Random getRandom() {
437          return random;
438      }
439  
440      /**
441       * creates a salted SHA-512 hash of the given byte array.
442       * 
443       * @param src
444       *            the source byte array to be hashed
445       * @return a salted SHA-512 hash of the given byte array
446       */
447      public static byte[] getSaltedHash(final byte[] src) {
448          if (lifecycleInstance.serverSalt.length == 0) {
449              return src;
450          }
451          byte[] salted = new byte[src.length
452                  + lifecycleInstance.serverSalt.length];
453          System.arraycopy(src, 0, salted, 0, src.length);
454          System.arraycopy(lifecycleInstance.serverSalt, 0, salted, src.length,
455                  lifecycleInstance.serverSalt.length);
456          return salted;
457      }
458  
459      /**
460       * initialises the Lifecyle singleton.
461       * 
462       * @param pulseWebapp
463       *            the root directory of the <em>pulse</em> webapp
464       */
465      public static synchronized void startup(final File pulseWebapp) { // NOPMD
466          if (lifecycleInstance == null) {
467              Lifecycle localLifecycle = new Lifecycle(pulseWebapp);
468              lifecycleInstance = localLifecycle;
469              localLifecycle.init();
470          } else {
471              throw new LifecycleException("Lifecycle has already been started.");
472          }
473          LOGGER.info("The Lifecycle has begun...");
474      }
475  
476      /**
477       * @return the Lifecycle singleton
478       */
479      @Deprecated
480      public static Lifecycle getInstance() {
481          if (lifecycleInstance == null) {
482              throw new LifecycleException("Lifecycle has not been started.");
483          }
484          return lifecycleInstance;
485      }
486  
487      /**
488       * shuts the Lifecycle singleton down.
489       */
490      public static synchronized void shutdown() { // NOPMD
491          if (lifecycleInstance == null) {
492              throw new LifecycleException("Lifecycle has not been started.");
493          }
494          lifecycleInstance.destroy();
495          lifecycleInstance = null; // NOPMD by thomas on 29.02.08 21:32
496          LOGGER.info("The Lifecycle has stopped...");
497      }
498  
499      /* --- START: protected direct access methods for Lifecycle tasks --------- */
500  
501      /**
502       * used by some Lifecycle tasks.
503       * 
504       * @return the root directory
505       */
506      protected File getPulseRootDir() {
507          return this.pulseRootDir;
508      }
509  
510      /**
511       * used by some Lifecycle tasks.
512       * 
513       * @return the locales
514       */
515      protected Collection<Locale> getLocalesDirectly() {
516          return this.locales;
517      }
518  
519      /**
520       * used by some Lifecycle tasks.
521       * 
522       * @return the components
523       */
524      protected Set<Component> getComponentsDirectly() {
525          return this.components;
526      }
527  
528      /**
529       * used by {@code LifecycleHibernateTasks}.
530       * 
531       * @return the bundles
532       */
533      protected ConcurrentHashMap<String, Bundle> getBundlesDirectly() {
534          return this.bundles;
535      }
536  
537      /**
538       * used by {@code LifecycleHibernateTasks}.
539       * 
540       * @return the data source
541       */
542      protected HibernateDataSourceImpl getHibernateDatasourceDirectly() {
543          return this.hibernateDataSource;
544      }
545  
546      /**
547       * used by {@code LifecycleHibernateTasks} to inject the data source.
548       * 
549       * @param ds
550       *            the data source
551       */
552      protected void setHibernateDataSourceDirectly(
553              final HibernateDataSourceImpl ds) {
554          this.hibernateDataSource = ds;
555      }
556  
557      /**
558       * used by {@code LifecycleMailQueueTasks}.
559       * 
560       * @return the mail queue
561       */
562      protected MailQueue getMailQueueDirectly() {
563          return this.mailQueue;
564      }
565  
566      /**
567       * used by {@code LifecycleMailQueueTasks} to inject the mail queue.
568       * 
569       * @param mc
570       *            the mail queue
571       */
572      protected void setMailQueueDirectly(final MailQueue mc) {
573          this.mailQueue = mc;
574      }
575  
576      /**
577       * gives internal access to the pulse configuration.
578       * 
579       * @return the pulse configuration
580       */
581      protected ServletConfig getPulseConfiguration() {
582          return (ServletConfig) PoorMansCache.getConfig(new File(
583                  this.coreConfigDir, "pulse.xml"));
584      }
585  
586      /**
587       * sets the JAXBContext.
588       * 
589       * @param ctx
590       *            the JAXBContext to set.
591       */
592      protected void setJAXBContext(final JAXBContext ctx) {
593          this.jaxbContext = ctx;
594      }
595  
596      /**
597       * sets the JSONJAXBContext.
598       * 
599       * @param context
600       *            the JSONJAXBContext to be set
601       */
602      protected void setJSONJAXBContext(final JSONJAXBContext context) {
603          this.jsonJaxbContext = context;
604      }
605  
606      /**
607       * sets the initialised {@code AttributeFactory} during startup.
608       * 
609       * @param factory
610       *            the factory to be set
611       */
612      protected void setAttributeFactoryDirectly(final AttributeFactory factory) {
613          this.attributeFactory = factory;
614      }
615  
616      /**
617       * converts the given list of {@code
618       * org.torweg.pulse.service.request.Locale} to a list of {@code
619       * java.util.Locale}.
620       * 
621       * @param collection
622       *            the list to be converted
623       * @param onlyActive
624       *            flag indicating whether to return only active locales
625       * @return the converted list
626       * @see Locale
627       */
628      protected static Collection<java.util.Locale> convertLocaleList(
629              final Collection<Locale> collection, final boolean onlyActive) {
630          Set<java.util.Locale> locs = new HashSet<java.util.Locale>();
631          for (Locale l : collection) {
632              if (onlyActive && l.isInactive()) {
633                  continue;
634              }
635              locs.add(LocaleManager.localeToLocale(l));
636          }
637          return locs;
638      }
639  
640      /* --- END: protected direct access methods for Lifecycle tasks --------- */
641  
642      /**
643       * actually performs the initialisation of the Lifecycle.
644       */
645      private void init() {
646  
647          /* create random source */
648          createRandom();
649  
650          /* initialise the JAXBContext */
651          LifecycleJAXBTasks.initialiseJAXBContext(this);
652  
653          /* create config pool */
654          initialiseLocalCache(this);
655  
656          /* initialise the JobletScheduler */
657          initializeJobletScheduler();
658  
659          /* identify and initialise components */
660          initialiseComponents();
661  
662          /* identify bundles */
663          List<File> bundleDirs = identifyBundles();
664  
665          /* initialise bundles */
666          LifecycleBundleTasks.initialiseBundles(bundleDirs, this);
667  
668          /* re-initialise the JAXBContext */
669          LifecycleJAXBTasks.initialiseJAXBContext(this);
670  
671          /* initialise Hibernate */
672          LifecycleHibernateTasks.initialiseHibernate(this);
673  
674          /* initialise ContentRegistry */
675          LifecycleHibernateTasks.initialiseRegistries(this);
676  
677          /* process annotations */
678          LifecycleBundleTasks.processControllerAnnotations(this);
679  
680          /* initialise users and groups */
681          LifecycleAccessControlTasks.initialiseUsersAndGroups(this);
682  
683          /* initialise attribute factory */
684          LifecycleAttributeTasks.initializeAttributeFactory(this);
685  
686          /* read main XSL */
687          try {
688              PoorMansCache.getXSLHandle(new File(this.pulseRootDir
689                      .getAbsolutePath()
690                      + File.separator
691                      + "WEB-INF"
692                      + File.separator
693                      + "xsl"
694                      + File.separator + "main.xsl"));
695          } catch (ConfigurationException e) {
696              LOGGER.error(e.getLocalizedMessage(), e);
697          }
698  
699          /* start the mail queue */
700          LifecycleMailQueueTasks.startMailQueue(this);
701  
702          /* start the IP to country locator */
703          startGeoLocationProvider(this);
704  
705          /* initialises the captcha-adapter */
706          initializeCaptchaAdapter(this);
707  
708          /* initialises the fop-factory-instance */
709          initializeFopFactoryInstance(this);
710          
711          /* initialises the VFS */
712          initialiseVirtualFileSystem();
713  
714          /* start the scheduler */
715          JobletScheduler.resume();
716  
717          /* start the WatchDog, if needed */
718          startWatchDog();
719  
720      }
721  
722      /**
723       * actually performs the shut down process.
724       */
725      private void destroy() {
726          /* stop the watchdog */
727          stopWatchDog(this);
728  
729          /* pause scheduler before stopping bundles */
730          JobletScheduler.pause();
731  
732          /* stop the bundles */
733          LifecycleBundleTasks.stopBundles(this);
734  
735          /* stop the JobletScheduler */
736          stopJobletScheduler();
737  
738          /* stop the mail queue */
739          LifecycleMailQueueTasks.stopMailQueue(this);
740  
741          /* close the hibernate datasource */
742          if (this.hibernateDataSource != null) {
743              this.hibernateDataSource.close();
744          } else {
745              LOGGER.warn("The HibernateDataSource was null.");
746          }
747  
748          /* stop the ip to country service */
749          if (this.geoLocationProvider != null) {
750              this.geoLocationProvider.shutdown();
751          } else {
752              LOGGER.info("The GeoLocationProvider was null.");
753          }
754  
755          /* stop the config pool */
756          stopLocalCache();
757      }
758  
759      /**
760       * creates the default random source.
761       */
762      private void createRandom() {
763          try {
764              random = SecureRandom.getInstance("SHA1PRNG");
765          } catch (NoSuchAlgorithmException e) {
766              LOGGER.warn("Not using SecureRandom: {}", e.getLocalizedMessage());
767              random = new Random();
768              random.setSeed(System.currentTimeMillis());
769          }
770      }
771  
772      /**
773       * stops the LocalCache.
774       */
775      private void stopLocalCache() {
776          LOGGER.trace("Stopping the local cache...");
777          if (PoorMansCache.getInstance() != null) {
778              ((PoorMansCache) PoorMansCache.getInstance()).shutdown();
779          } else {
780              LOGGER.info("PoorMansCache was null.");
781          }
782          this.locales = null; // NOPMD by thomas on 29.02.08 21:31
783          LOGGER.info("Local cache stopped.");
784      }
785  
786      /**
787       * stops the JobletScheduler.
788       */
789      private void stopJobletScheduler() {
790          LOGGER.trace("Stopping the JobletScheduler...");
791          try {
792              JobletScheduler.stop();
793          } catch (Exception e) {
794              LOGGER.error("Cannot stop the JobletScheduler: ", e);
795          }
796      }
797  
798      /**
799       * initialises the WatchDog, if reloading is switched on.
800       */
801      private void startWatchDog() {
802          ServletConfig servletConfig = getPulseConfiguration();
803          if (servletConfig.isReloadable()) {
804              LOGGER.trace("Initialising the WatchDog...");
805              this.timer = new Timer();
806              this.watchDog = new WatchDog();
807              this.timer.scheduleAtFixedRate(this.watchDog, servletConfig
808                      .getReloadInterval(), servletConfig.getReloadInterval());
809              LOGGER.info("WatchDog initialised.");
810          }
811      }
812  
813      /**
814       * stops the WatchDog.
815       * 
816       * @param lc
817       *            the lifecycle
818       */
819      private void stopWatchDog(final Lifecycle lc) {
820          if (lc.timer != null) {
821              LOGGER.trace("Stopping the WatchDog...");
822              lc.timer.cancel();
823              long start = System.currentTimeMillis();
824              /* wait up to 30 seconds for the WatchDog to finish */
825              while (lc.watchDog.isRunning()
826                      && (System.currentTimeMillis() - start < 30000)) {
827                  try {
828                      Thread.sleep(100);
829                  } catch (InterruptedException e) {
830                      LOGGER.trace("Error stopping the WatchDog: {}", e
831                              .getLocalizedMessage());
832                  }
833              }
834              LOGGER.info("WatchDog stopped.");
835          } else {
836              LOGGER.info("The WatchDog has not been started.");
837          }
838      }
839  
840      /**
841       * initialise the LocalCache.
842       * 
843       * @param lc
844       *            the lifecycle instance
845       */
846      private void initialiseLocalCache(final Lifecycle lc) {
847          LOGGER.trace("Initialising local cache...");
848          PoorMansCache.init(this.coreConfigDir);
849          PoorMansCache.getInstance();
850  
851          /* get the servlet's configuration */
852          ServletConfig pulseConfiguration = getPulseConfiguration();
853          LOGGER.info("Local cache initialised with a reload interval of {} ms.",
854                  pulseConfiguration.getReloadInterval());
855          try {
856              lc.serverSalt = pulseConfiguration.getServerSalt()
857                      .getBytes("utf-8");
858          } catch (UnsupportedEncodingException e) {
859              LOGGER.warn(e.getLocalizedMessage());
860              lc.serverSalt = pulseConfiguration.getServerSalt().getBytes();
861          }
862          lc.locales = LocaleManager.getInstance().getLocales();
863      }
864  
865      /**
866       * starts the JobletScheduler.
867       */
868      private void initializeJobletScheduler() {
869          LOGGER.trace("Initialising the JobletScheduler...");
870          try {
871              JobletScheduler.startPaused(getPulseConfiguration()
872                      .getJobletSchedulerConfiguration());
873          } catch (Exception exception) {
874              LOGGER.error("Cannot initialise the JobletScheduler: ", exception);
875          }
876      }
877  
878      /**
879       * initialises the IP2CountryProvider.
880       * 
881       * @param lc
882       *            the lifecycle
883       */
884      private void startGeoLocationProvider(final Lifecycle lc) {
885          /* initialise the IP2CountryProvider */
886          try {
887              LOGGER.debug("Starting GeoLocationProvider...");
888              IGeoLocationProvider locationProvider = getPulseConfiguration()
889                      .getGeoLocationProvider().newInstance();
890              locationProvider.startup();
891              lc.geoLocationProvider = locationProvider;
892              LOGGER.info("GeoLocationProvider [{}] started.", locationProvider
893                      .getClass().getCanonicalName());
894          } catch (Exception e) {
895              LOGGER.error("Could not setup GeoLocationProvider, "
896                      + "using NoLookupLocationProvider instead: {}", e
897                      .getLocalizedMessage());
898              this.geoLocationProvider = new NoLookupLocationProvider();
899          }
900      }
901  
902      /**
903       * identifies all component directories.
904       * 
905       * @return the list of component directories
906       */
907      private List<File> indentifyComponents() {
908          LOGGER.trace("Identifying components...");
909  
910          ArrayList<File> identifiedComponents = new ArrayList<File>();
911          File[] entries = new File(this.pulseRootDir, "WEB-INF" + File.separator
912                  + "components").listFiles();
913          if (entries == null) {
914              entries = new File[1];
915          }
916  
917          /* search all subdirectories of the components directory */
918          for (File entry : entries) {
919              if (entry.isDirectory()) {
920                  LOGGER.trace("Checking component '" + entry.getName()
921                          + "' in '" + entry.getAbsolutePath() + "'.");
922                  if (new File(entry, "component.xml").exists()) {
923                      identifiedComponents.add(entry);
924                      LOGGER.debug("Found component '" + entry.getName()
925                              + "' in '" + entry.getAbsolutePath() + "'.");
926                  } else {
927                      LOGGER.warn("Missing 'component.xml' in '"
928                              + entry.getAbsolutePath()
929                              + "' -> Component will not be initialised.");
930                  }
931  
932              }
933          }
934  
935          /* are there any components? */
936          if (identifiedComponents.isEmpty()) {
937              LOGGER.error("The container contains no components!");
938          }
939          return identifiedComponents;
940      }
941  
942      /**
943       * initialises the components of the container.
944       */
945      private void initialiseComponents() {
946          Set<Component> localComponents = new HashSet<Component>();
947          Unmarshaller unmarshaller;
948          try {
949              unmarshaller = this.jaxbContext.createUnmarshaller();
950          } catch (JAXBException e) {
951              throw new LifecycleException("Error initialising Unmarshaller: "
952                      + e.getLocalizedMessage(), e);
953          }
954  
955          for (File configFile : indentifyComponents()) {
956              try {
957                  localComponents.add((Component) unmarshaller
958                          .unmarshal(new File(configFile, "component.xml")));
959              } catch (JAXBException e) {
960                  throw new LifecycleException("Error initialising component ["
961                          + configFile.getName() + "]: "
962                          + e.getLocalizedMessage(), e);
963              }
964          }
965  
966          this.components.clear();
967          this.components.addAll(localComponents);
968      }
969  
970      /**
971       * identifies all bundle directories.
972       * 
973       * @return the list of bundle directories
974       */
975      private List<File> identifyBundles() {
976          LOGGER.trace("Identifying bundles...");
977  
978          ArrayList<File> identifiedBundles = new ArrayList<File>();
979          File[] entries = this.bundlesRootDir.listFiles();
980          if (entries == null) {
981              entries = new File[1];
982          }
983  
984          /* search all subdirectories of the bundle directory */
985          for (File entry : entries) {
986              if (entry.isDirectory()) {
987                  LOGGER.trace("Checking bundle '" + entry.getName() + "' in '"
988                          + entry.getAbsolutePath() + "'.");
989                  if (new File(entry, "bundle.xml").exists()) {
990                      identifiedBundles.add(entry);
991                      LOGGER.debug("Found bundle '" + entry.getName() + "' in '"
992                              + entry.getAbsolutePath() + "'.");
993                  } else {
994                      LOGGER.warn("Missing 'bundle.xml' in '"
995                              + entry.getAbsolutePath()
996                              + "' -> Bundle will not be loaded.");
997                  }
998  
999              }
1000         }
1001 
1002         /* are there any bundles? */
1003         if (identifiedBundles.isEmpty()) {
1004             LOGGER.error("The container contains no bundles!");
1005         }
1006         return identifiedBundles;
1007     }
1008 
1009     /**
1010      * initialises the {@code VirtualFileSystem}.
1011      */
1012     private void initialiseVirtualFileSystem() {
1013         VirtualFileSystem.init(getPulseConfiguration().getVFSConfiguration());
1014         LOGGER.info("Initialised VirtualFileSystem.");
1015     }
1016 
1017     /**
1018      * Initialises the captcha-adapter.
1019      * 
1020      * @param lc
1021      *            the {@code Lifecycle}
1022      */
1023     private void initializeCaptchaAdapter(final Lifecycle lc) {
1024         /* initialise the IP2CountryProvider */
1025         try {
1026 
1027             if (getPulseConfiguration().getCaptchaAdapter() != null) {
1028 
1029                 LOGGER.debug("Setting-up captcha-adapter...");
1030 
1031                 ICaptchaAdapter<Configuration> adapter = getPulseConfiguration()
1032                         .getCaptchaAdapter().newInstance();
1033 
1034                 // configure adapter
1035                 adapter.initialize(PoorMansCache.getConfiguration(new File(
1036                         this.coreConfigDir, adapter.getClass()
1037                                 .getCanonicalName()
1038                                 + ".xml")));
1039 
1040                 lc.captchaAdapter = adapter;
1041 
1042                 LOGGER.info("Captcha-adapter [{}] set up.", adapter.getClass()
1043                         .getCanonicalName());
1044 
1045             } else {
1046                 LOGGER
1047                         .info("Captcha-adapter: Not configured! Not available! Howto: "
1048                                 + "Add <captcha-adapter class=\"your.available.implementation.of.ICaptchaAdapter\"/> "
1049                                 + "to pulse.xml. "
1050                                 + "Add the your.available.implementation.of.ICaptchaAdapter.xml to WEB-INF/conf.");
1051             }
1052 
1053         } catch (Exception e) {
1054             LOGGER.error("Setting-up captcha-adapter has failed: {}", e
1055                     .getLocalizedMessage());
1056         }
1057     }
1058 
1059     /**
1060      * Initialises the {@code FopFactory}-instance.
1061      * 
1062      * @param lc
1063      *            the {@code Lifecycle}
1064      */
1065     private void initializeFopFactoryInstance(final Lifecycle lc) {
1066         try {
1067             // retrieve fop-factory-instance
1068             FopFactory fopFactory = FopFactory.newInstance();
1069             // configure the fop-factory if possible
1070             if (getPulseConfiguration().getFopPath() != null) {
1071                 File file = new File(getBasePath(), getPulseConfiguration()
1072                         .getFopPath());
1073                 // try to configure fop-factory
1074                 if (file.exists()) {
1075                     File fopConf = new File(file, "fop.conf.xml");
1076                     if (fopConf.exists()) {
1077                         fopFactory
1078                                 .setUserConfig(new File(file, "fop.conf.xml"));
1079                         LOGGER.info(
1080                                 "Configured FopFactory with configuration: {}",
1081                                 fopConf.toURL());
1082                     }
1083                     fopFactory.setBaseURL(file.toURL().toString());
1084                     fopFactory.setFontBaseURL(file.toURL().toString());
1085                 }
1086             }
1087             // set fop-factory for lc
1088             lc.fopFactoryInstance = fopFactory;
1089             LOGGER.info("FopFactory uses base-url: " + fopFactory.getBaseURL());
1090             LOGGER.info("FopFactory uses font-base-url: "
1091                     + fopFactory.getFontBaseURL());
1092         } catch (Exception e) {
1093             LOGGER.error("Initialising FopFactory has failed..."
1094                     + e.getLocalizedMessage(), e);
1095         }
1096     }
1097 
1098     /**
1099      * @return Returns the root-directory for the bundles. (for WatchDog)
1100      */
1101     protected File getBundlesRootDir() {
1102         return this.bundlesRootDir;
1103     }
1104 
1105     /**
1106      * a timer task used to check for changed resources.
1107      */
1108     private class WatchDog extends TimerTask {
1109 
1110         /**
1111          * flag, indicating whether the task is running.
1112          */
1113         private boolean running = false;
1114 
1115         /**
1116          * checks for modified resources.
1117          */
1118         @Override
1119         public void run() {
1120             synchronized (this) {
1121                 if (this.running) {
1122                     return;
1123                 }
1124                 this.running = true;
1125             }
1126             try {
1127                 NDC.push("watchdog");
1128                 LOGGER.trace("WatchDog: run()");
1129                 /* check config pool */
1130                 boolean cacheChanges = checkLocalCache((PoorMansCache) PoorMansCache
1131                         .getInstance());
1132 
1133                 /* check bundles */
1134                 boolean bundleChanges = checkBundles();
1135 
1136                 /*
1137                  * changes in cache or bundles --> rebuild JAXBContext and
1138                  * process annotations
1139                  */
1140                 if (cacheChanges || bundleChanges) {
1141                     LifecycleJAXBTasks.initialiseJAXBContext(lifecycleInstance);
1142                     LifecycleBundleTasks
1143                             .processControllerAnnotations(lifecycleInstance);
1144                 }
1145 
1146                 /* check IP to country locator */
1147                 if (geoLocationProvider.isModified()) {
1148                     LOGGER.info("restarting GeoLocationProvider.");
1149                     geoLocationProvider.restart();
1150                 }
1151             } finally {
1152                 synchronized (this) {
1153                     this.running = false;
1154                 }
1155                 NDC.pop();
1156                 NDC.remove();
1157             }
1158 
1159         }
1160 
1161         /**
1162          * returns whether the {@code WatchDog} is running.
1163          * 
1164          * @return {@code true}, if the {@code WatchDog} is running. Otherwise
1165          *         {@code false}.
1166          */
1167         public final boolean isRunning() {
1168             return this.running;
1169         }
1170 
1171         /**
1172          * checks the config pool for changes.
1173          * 
1174          * @param pool
1175          *            the config pool
1176          * @return {@code true}, if changes occurred
1177          */
1178         private boolean checkLocalCache(final PoorMansCache pool) {
1179             if (pool.isModified()) {
1180                 pool.restart();
1181                 LOGGER.info("WatchDog: Config pool reloaded.");
1182                 /* reconfigure MailQueue */
1183                 reconfigureMailQueue();
1184                 /* reload IP2CountryLocator, if necessary */
1185                 geoLocationProvider.restart();
1186                 return true;
1187             }
1188             return false;
1189         }
1190 
1191         /**
1192          * reconfigure the MailQueue.
1193          */
1194         private void reconfigureMailQueue() {
1195             MailQueueConfiguration conf = new MailQueueConfiguration();
1196             conf.init(getPulseConfiguration().getMailQueueConfiguration());
1197             conf.setPulseRootDir(getBasePath());
1198             getMailQueue().reconfigure(conf);
1199         }
1200 
1201         /**
1202          * checks the bundles for changes and reloads if necessary.
1203          * 
1204          * @return {@code true}, if changes occurred
1205          */
1206         private boolean checkBundles() {
1207             File[] bundleDirectories = getBundlesRootDir().listFiles();
1208             if (bundleDirectories == null) {
1209                 LOGGER
1210                         .debug("Bundle check aborted: Could not list bundle directory.");
1211                 return false;
1212             }
1213             ArrayList<File> possibleBundles = new ArrayList<File>(Arrays
1214                     .asList(bundleDirectories));
1215             /* check previously loaded bundles first */
1216             boolean initializedBundles = checkInitializedBundles(possibleBundles);
1217 
1218             /* check remaining directory entries from the bundle root directory */
1219             boolean newBundles = checkNewBundles(possibleBundles);
1220 
1221             return (initializedBundles | newBundles);
1222         }
1223 
1224         /**
1225          * checks all newly found {@code Bundle}s and tries to start them up.
1226          * 
1227          * @param newBundles
1228          *            the new bundles
1229          * @return {@code true}, if changes occurred
1230          */
1231         private boolean checkNewBundles(final List<File> newBundles) {
1232             boolean changed = false;
1233             for (File f : newBundles) {
1234                 if ((f.isDirectory()) && (new File(f, "bundle.xml").exists())) {
1235                     try {
1236                         Bundle bundle = LifecycleBundleTasks.initBundle(f,
1237                                 lifecycleInstance);
1238                         lifecycleInstance.bundles.put(bundle.getName(), bundle);
1239                         LOGGER.info("WatchDog: Bundle '{}' loaded.", bundle
1240                                 .getName());
1241                         changed = true;
1242                     } catch (LifecycleException e) {
1243                         LOGGER.error("WatchDog: Bundle defined in '"
1244                                 + f.getAbsolutePath() + "' cannot be loaded.",
1245                                 e);
1246                     }
1247                 }
1248             }
1249             return changed;
1250         }
1251 
1252         /**
1253          * checks all initialised {@code Bundle}s for modifications and reloads
1254          * them, if necessary.
1255          * 
1256          * @param initializedBundles
1257          *            the initialised bundles
1258          * @return {@code true}, if modifications were detected
1259          */
1260         private boolean checkInitializedBundles(
1261                 final List<File> initializedBundles) {
1262             boolean changed = false;
1263             for (Bundle bundle : getBundles()) {
1264                 if (!bundle.getDirectory().exists()) {
1265                     /* bundle does not exist anymore */
1266                     PoorMansCache.flushBundle(bundle);
1267                     changed = true;
1268                 } else if (bundle.isModified()) {
1269                     PoorMansCache.flushBundle(bundle);
1270                     /* bundle has been modified */
1271                     initializedBundles.remove(bundle.getDirectory());
1272                     try {
1273                         bundle.restart();
1274                         LOGGER.info("WatchDog: Bundle '{}' reloaded.", bundle
1275                                 .getName());
1276                         changed = true;
1277                     } catch (LifecycleException e) {
1278                         LOGGER.error("WatchDog: Bundle '" + bundle.getName()
1279                                 + "' cannot be reloaded.", e);
1280                     }
1281                 } else {
1282                     /* bundle exists and has not been modified */
1283                     initializedBundles.remove(bundle.getDirectory());
1284                 }
1285             }
1286             return changed;
1287         }
1288     }
1289 
1290 }
1291