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.bundle;
19   
20   import java.io.File;
21   import java.io.Serializable;
22   import java.util.ArrayList;
23   import java.util.Collection;
24   import java.util.Collections;
25   import java.util.HashMap;
26   import java.util.HashSet;
27   import java.util.List;
28   import java.util.Map;
29   import java.util.Set;
30   import java.util.TreeMap;
31   import java.util.TreeSet;
32   
33   import javax.persistence.Basic;
34   import javax.persistence.Column;
35   import javax.persistence.Entity;
36   import javax.persistence.GeneratedValue;
37   import javax.persistence.Id;
38   import javax.persistence.Transient;
39   import javax.xml.bind.annotation.XmlAccessOrder;
40   import javax.xml.bind.annotation.XmlAccessType;
41   import javax.xml.bind.annotation.XmlAccessorOrder;
42   import javax.xml.bind.annotation.XmlAccessorType;
43   import javax.xml.bind.annotation.XmlAttribute;
44   import javax.xml.bind.annotation.XmlRootElement;
45   import javax.xml.bind.annotation.XmlTransient;
46   
47   import net.sf.json.JSONObject;
48   
49   import org.jdom.Element;
50   import org.jdom.input.SAXBuilder;
51   import org.slf4j.Logger;
52   import org.slf4j.LoggerFactory;
53   import org.torweg.pulse.bundle.Controller.AlwaysRun;
54   import org.torweg.pulse.configuration.ConfigurationException;
55   import org.torweg.pulse.configuration.PoorMansCache;
56   import org.torweg.pulse.invocation.lifecycle.FileResource;
57   import org.torweg.pulse.invocation.lifecycle.LifecycleException;
58   import org.torweg.pulse.invocation.lifecycle.LifecycleResource;
59   import org.torweg.pulse.service.request.CommandBuilder;
60   import org.torweg.pulse.site.View;
61   import org.torweg.pulse.site.ViewTypes;
62   import org.torweg.pulse.util.ClassComperator;
63   
64   /**
65    * a bundle in the <em>pulse</em> web application framework.
66    * 
67    * @author Thomas Weber, Christian Schatt
68    * @version $Revision: 2071 $
69    */
70   // CHECKSTYLE:OFF
71   @Entity
72   @XmlRootElement(name = "bundle")
73   @XmlAccessorOrder(XmlAccessOrder.UNDEFINED)
74   @XmlAccessorType(XmlAccessType.FIELD)
75   public final class Bundle implements Serializable, LifecycleResource {
76       // CHECKSTYLE:ON
77   
78       /**
79        * serialVersionUID.
80        */
81       private static final long serialVersionUID = -8007500206195229474L;
82   
83       /**
84        * the logger.
85        */
86       private static final Logger LOGGER = LoggerFactory.getLogger(Bundle.class);
87   
88       /**
89        * The unique, generated id of this {@code Bundle}.
90        */
91       @Id
92       @GeneratedValue
93       @Column(nullable = false, unique = true, updatable = false)
94       @XmlAttribute(name = "id")
95       private final Long id = null;
96   
97       /**
98        * the bundle's name.
99        */
100      @Basic
101      @Column(unique = true)
102      @XmlAttribute(name = "name", required = true)
103      private String name;
104  
105      /**
106       * reload triggering resources of the Bundle.
107       */
108      @Transient
109      @XmlTransient
110      private final List<LifecycleResource> watchResources = new ArrayList<LifecycleResource>();
111  
112      /**
113       * the controllers associated with the bundle.
114       */
115      @Transient
116      @XmlTransient
117      private List<Controller> controllers;
118  
119      /**
120       * the root directory of the bundle.
121       */
122      @Transient
123      @XmlTransient
124      private File baseDir;
125  
126      /**
127       * the last modified timestamp of the root directory.
128       */
129      @Transient
130      @XmlTransient
131      private long baseDirLastModified;
132  
133      /**
134       * the bundle.xml as a JDOM Element.
135       */
136      @Transient
137      @XmlTransient
138      private Element bundleXML;
139  
140  //  /**
141  //   * The annotated classes for Hibernate<sup>TM</sup>.
142  //   */
143  //  @Transient
144  //  @XmlTransient
145  //  private List<Class<? extends Object>> hibernateClasses;
146  
147      /**
148       * the contents available in the bundle.
149       */
150      @Transient
151      @XmlTransient
152      private final Set<Class<? extends Object>> contentTypes = new TreeSet<Class<? extends Object>>(
153              new ClassComperator());;
154  
155      /**
156       * maps the different view types for every content.
157       */
158      @Transient
159      @XmlTransient
160      private final Map<Class<? extends Object>, ViewTypes> contentViews = new TreeMap<Class<? extends Object>, ViewTypes>(
161              new ClassComperator());
162  
163  //  /**
164  //   * the colon separated string of JAXB packages.
165  //   */
166  //  @Transient
167  //  @XmlTransient
168  //  private String jaxbPackages;
169  
170      /**
171       * the action configuration map.
172       */
173      @Transient
174      @XmlTransient
175      private Map<String, ActionConfiguration> actionMap = new HashMap<String, ActionConfiguration>();
176  
177      /**
178       * the any action configuration set for pre-action execution.
179       */
180      @Transient
181      @XmlTransient
182      private Set<ControllerMethodConfiguration> anyActionPreSet = new HashSet<ControllerMethodConfiguration>();
183  
184      /**
185       * the any action configuration set for post-action execution.
186       */
187      @Transient
188      @XmlTransient
189      private Set<ControllerMethodConfiguration> anyActionPostSet = new HashSet<ControllerMethodConfiguration>();
190  
191      /**
192       * the set of actions requiring tokens.
193       */
194      @Transient
195      @XmlTransient
196      private Collection<String> actionsRequiringTokens = new HashSet<String>();
197  
198      /**
199       * used by Hibernate<sup>TM</sup>.
200       */
201      @Deprecated
202      protected Bundle() {
203          super();
204      }
205  
206      /**
207       * constructs a new startable bundle with the given name.
208       * 
209       * @param dir
210       *            the bundle's root directory
211       */
212      public Bundle(final File dir) {
213          this.baseDir = dir;
214          this.baseDirLastModified = this.baseDir.lastModified();
215          this.name = dir.getName();
216  //      this.hibernateClasses = new ArrayList<Class<? extends Object>>();
217      }
218  
219      /**
220       * returns the primary key.
221       * 
222       * @return the primary key
223       */
224      public Long getId() {
225          return this.id;
226      }
227  
228      /**
229       * @return the name of the Bundle
230       */
231      public String getName() {
232          return this.name;
233      }
234  
235      /**
236       * @return the controllers of the Bundle
237       */
238      public List<Controller> getControllers() {
239          return this.controllers;
240      }
241  
242      /**
243       * returns the actions requiring {@code Token}s.
244       * 
245       * @return the actions requiring {@code Token}s
246       */
247      public Collection<String> getActionsRequiringTokens() {
248          return this.actionsRequiringTokens;
249      }
250  
251      /**
252       * sets the annotation based controller mappings.
253       * 
254       * @param actionConfs
255       *            the action configurations
256       * @param anyActionPreConfs
257       *            the method configuration for any action methods (PRE)
258       * @param anyActionPostConfs
259       *            the method configuration for any action methods (POST)
260       * @param requireTokenActions
261       *            the actions requiring tokens
262       * @see Controller.AlwaysRun
263       */
264      public void setControllerMappings(
265              final Map<String, ActionConfiguration> actionConfs,
266              final Set<ControllerMethodConfiguration> anyActionPreConfs,
267              final Set<ControllerMethodConfiguration> anyActionPostConfs,
268              final Set<String> requireTokenActions) {
269          this.actionMap = actionConfs;
270          this.anyActionPreSet = anyActionPreConfs;
271          this.anyActionPostSet = anyActionPostConfs;
272          this.actionsRequiringTokens = requireTokenActions;
273      }
274  
275      /**
276       * returns the action configurations of the bundle.
277       * 
278       * @return the action configurations
279       */
280      public Collection<ActionConfiguration> getActionConfigurations() {
281          return this.actionMap.values();
282      }
283  
284      /**
285       * returns the {@code ActionConfiguration} for the given action.
286       * 
287       * @param action
288       *            the action
289       * @return the {@code ActionConfiguration}, or {@code null}, if no such
290       *         configuration exists.
291       */
292      public ActionConfiguration getActionConfiguration(final String action) {
293          return this.actionMap.get(action);
294      }
295  
296      /**
297       * returns the no-action configurations of the specified mode.
298       * 
299       * @param mode
300       *            the mode of the no-action configurations requested
301       * @return an unmodifiable view of the no-action configurations
302       */
303      public Set<ControllerMethodConfiguration> getAnyActionConfigurations(
304              final AlwaysRun mode) {
305          if (mode == AlwaysRun.PRE) {
306              return Collections.unmodifiableSet(this.anyActionPreSet);
307          } else if (mode == AlwaysRun.POST) {
308              return Collections.unmodifiableSet(this.anyActionPostSet);
309          }
310          return new HashSet<ControllerMethodConfiguration>();
311      }
312  
313      /**
314       * @return the root directory of the Bundle
315       */
316      public File getDirectory() {
317          return this.baseDir;
318      }
319  
320      /**
321       * @return the {@code Content} types available from the {@code Bundle}
322       */
323      public Set<Class<? extends Object>> getContentTypes() {
324          return this.contentTypes;
325      }
326  
327      /**
328       * Returns the {@code View}s of the {@code Bundle} for the given {@code
329       * Content}.
330       * 
331       * @param object
332       *            the {@code object} to check against
333       * @return the {@code View}s of the {@code Bundle} for the given {@code
334       *         Content}.
335       */
336      public ViewTypes getViewTypes(final Object object) {
337          if (this.contentTypes.contains(object.getClass())) {
338              return this.contentViews.get(object.getClass());
339          }
340          return null;
341      }
342  
343      /**
344       * indicates whether this Resource or any of its Sub-Resources has been
345       * modified.
346       * 
347       * @return {@code true}, if the resource has been modified
348       */
349      public boolean isModified() {
350          synchronized (this) {
351              if (this.baseDir.lastModified() != this.baseDirLastModified) {
352                  return true;
353              }
354              for (LifecycleResource resource : this.watchResources) {
355                  if (resource.isModified()) {
356                      return true;
357                  }
358              }
359              return false;
360          }
361      }
362  
363      /**
364       * @see Object#hashCode()
365       * @return the hash code
366       */
367      @Override
368      public int hashCode() {
369          return this.name.hashCode();
370      }
371  
372      /**
373       * compares the given object with the {@code Bundle}.
374       * <p>
375       * If the object is not an instance of {@code Bundle}, the method returns
376       * {@code false}. Otherwise, if both bundles have an initialised, id the
377       * bundles are compared by id. If one of the bundles does not have an
378       * initialised id, the bundles are compared by name.
379       * </p>
380       * 
381       * @param obj
382       *            the object to check against
383       * @return {@code true}, if the given object is equal to the {@code Bundle}
384       */
385      @Override
386      public boolean equals(final Object obj) {
387          if (obj instanceof Bundle) {
388              Bundle compareBundle = (Bundle) obj;
389              if ((compareBundle.id != null) && (this.id != null)) {
390                  return this.id.equals(compareBundle.id);
391              }
392              return this.name.equals(compareBundle.name);
393          }
394          return false;
395      }
396  
397      /**
398       * starts the bundle.
399       * <p>
400       * <ol type="1">
401       * <li>the "bundle.xml" is read</li>
402       * <li>all page flows of the bundle are pre-initialised</li>
403       * <li>the BundleClassLoader is started</li>
404       * <li>the {@code SymbolResolver} for page flow symbols is set up</li>
405       * <li>if needed, a DataSourceFactory is started</li>
406       * <li>if defined in {@code &lt;watch-resources/&gt;}, these resources are
407       * added to the trigger list for reloads</li>
408       * </ol>
409       * </p>
410       */
411      public void startup() {
412          synchronized (this) {
413              LOGGER.trace("Starting bundle '{}' from '{}'...", new Object[] {
414                      this.name, this.baseDir });
415              /* read the bundle.xml */
416              this.bundleXML = readBundleXML();
417  
418              /* schedule joblets */
419              scheduleJoblets();
420  
421              /* initialise controllers */
422              this.controllers = initialiseControllers();
423  
424  
425              /* add content mappings */
426              initialiseContentMappings();
427  
428              /* gather additional watch-resources as defined in the bundle.xml */
429              // TODO
430              this.baseDirLastModified = this.baseDir.lastModified();
431              /* show status message */
432              LOGGER.debug("Bundle '{}' started.", this.name);
433          }
434      }
435  
436      /**
437       * stops the bundle.
438       * <p>
439       * <ol type="1">
440       * <li>if present, the DataSourceFactory is stopped</li>
441       * <li>the {@code SymbolResolver} is set to {@code null}</li>
442       * <li>the page flows of the bundle are set to {@code null}</li>
443       * <li>the bundle is flushed from the config pool</li>
444       * <li>the BundleClassLoader is stopped</li>
445       * <li>clean up the watch resources</li>
446       * <li>the bundle.xml is set to {@code null}</li>
447       * </ol>
448       * </p>
449       */
450      public void shutdown() {
451          synchronized (this) {
452              LOGGER.trace("Stopping bundle '{}'...", this.name);
453  
454              /* unschedule joblets */
455              unscheduleJoblets();
456  
457              /* clear the list of controllers */
458              if (this.controllers != null) {
459                  this.controllers.clear();
460              }
461  
462              /* flush config pool */
463              PoorMansCache.flushBundle(this);
464  
465              /* clean the watch-resources */
466              this.watchResources.clear();
467  
468              /* set the bundle.xml to null */
469              this.bundleXML = null; // NOPMD by thomas on 29.02.08 22:08
470  
471              /* display status message */
472              LOGGER.trace("Stopped bundle '{}'.", this.name);
473          }
474      }
475  
476      /**
477       * reloads the bundle.
478       */
479      public void restart() {
480          synchronized (this) {
481              shutdown();
482              startup();
483          }
484      }
485  
486      /**
487       * reads the bundle.xml.
488       * 
489       * @return the parsed bundle.xml as a JDOM Element
490       */
491      private Element readBundleXML() {
492          try {
493              Element config = new SAXBuilder().build(
494                      new File(this.baseDir, "bundle.xml")).getRootElement();
495              this.watchResources.add(new FileResource(new File(this.baseDir,
496                      "bundle.xml")));
497              return config;
498          } catch (Exception e) {
499              throw new LifecycleException(
500                      "Error processing bundle.xml for bundle '" + this.name
501                              + "' in '" + this.baseDir.getAbsolutePath() + "'.",
502                      e);
503          }
504      }
505  
506      /**
507       * Schedules the {@code Joblet}s listed in the bundle.xml.
508       */
509      private void scheduleJoblets() {
510          JobletScheduler jobletScheduler;
511          try {
512              jobletScheduler = JobletScheduler.getInstance();
513          } catch (Exception exception) {
514              LOGGER.error("Cannot get instance of joblet scheduler in bundle '"
515                      + this.name + "'.", exception);
516              return;
517          }
518          Element jobletsElem;
519          synchronized (this.bundleXML) {
520              jobletsElem = this.bundleXML.getChild("joblets");
521          }
522          if (jobletsElem == null) {
523              LOGGER.debug(
524                      "There are no joblets to be scheduled for bundle '{}'.",
525                      this.name);
526              return;
527          }
528          for (Object jobletObj : jobletsElem.getChildren("joblet")) {
529              try {
530                  @SuppressWarnings("unchecked")
531                  Class<? extends AbstractJoblet> jobletClass = (Class<? extends AbstractJoblet>) Class
532                          .forName(((Element) jobletObj)
533                                  .getAttributeValue("class"));
534                  for (Object cronExprObj : ((Element) jobletObj)
535                          .getChildren("cron-expression")) {
536                      try {
537                          jobletScheduler.schedule(this, jobletClass,
538                                  ((Element) cronExprObj).getText());
539                          LOGGER
540                                  .info(
541                                          "Scheduled joblet '{}' with cron expression '{}' for bundle '{}'.",
542                                          new Object[] {
543                                                  jobletClass.getCanonicalName(),
544                                                  ((Element) cronExprObj)
545                                                          .getText(), this.name });
546                      } catch (Exception exception) {
547                          LOGGER.error("Cannot schedule joblet '"
548                                  + jobletClass.getCanonicalName()
549                                  + "' with cron expression '"
550                                  + ((Element) cronExprObj).getText()
551                                  + "' for bundle '" + this.name + "'.",
552                                  exception);
553                      }
554                  }
555              } catch (NullPointerException exception) { // NOPMD
556                  LOGGER.error("The configuration of bundle '" + this.name
557                          + "' contains a 'joblet' element "
558                          + "without a 'class' attribute.", exception);
559              } catch (ClassNotFoundException exception) {
560                  LOGGER.error("The configuration of bundle '" + this.name
561                          + "' contains a 'joblet' element with a "
562                          + "'class' attribute whose value '"
563                          + ((Element) jobletObj).getAttributeValue("class")
564                          + "' is not a valid/known class name.", exception);
565              } catch (ClassCastException exception) {
566                  LOGGER.error("The configuration of bundle '" + this.name
567                          + "' contains a 'joblet' element with a "
568                          + "'class' attribute whose value '"
569                          + ((Element) jobletObj).getAttributeValue("class")
570                          + "' is the name of a class, that does not extend '"
571                          + AbstractJoblet.class.getCanonicalName() + "'.",
572                          exception);
573              }
574          }
575      }
576  
577      /**
578       * Unschedules the {@code Joblet}s that are scheduled for the {@code Bundle}
579       * .
580       */
581      private void unscheduleJoblets() {
582          try {
583              JobletScheduler.getInstance().unschedule(this);
584          } catch (Exception exception) {
585              LOGGER.error("Cannot unschedule joblets in bundle '" + this.name
586                      + "'.", exception);
587          }
588      }
589  
590      /**
591       * initialises all controllers of the bundle.
592       * 
593       * @return the list of initialised controllers
594       */
595      @SuppressWarnings("unchecked")
596      private List<Controller> initialiseControllers() {
597          ArrayList<Controller> localControllers = new ArrayList<Controller>();
598          ArrayList<?> controllerElements;
599          try {
600              controllerElements =  new ArrayList<Object>(this.bundleXML.getChild(
601                      "controllers").getChildren("controller"));
602          } catch (NullPointerException e) { // NOPMD
603              throw new LifecycleException("Bundle '" + this.name
604                      + "' does not contain any controllers.", e);
605          }
606          for (Object ce : controllerElements) {
607              Element controllerElement = (Element) ce;
608              try {
609                  Controller controller = (Controller) Class.forName(
610                          controllerElement.getAttributeValue("class"))
611                          .newInstance();
612                  /* configure always run */
613                  if (controllerElement.getAttributeValue("alwaysRun") != null) {
614                      String value = controllerElement
615                              .getAttributeValue("alwaysRun");
616                      if (value.equals("pre")) {
617                          controller.setAlwaysRun(AlwaysRun.PRE);
618                      } else if (value.equals("post")) {
619                          controller.setAlwaysRun(AlwaysRun.POST);
620                      } else if (value.equals("false")) {
621                          controller.setAlwaysRun(AlwaysRun.FALSE);
622                      } else {
623                          throw new ConfigurationException("The controller '"
624                                  + controllerElement.getAttributeValue("class")
625                                  + "' in bundle '" + this.name
626                                  + "' has an undefined value "
627                                  + "for its alwaysRun attribute.");
628                      }
629                  } else {
630                      controller.setAlwaysRun(AlwaysRun.FALSE);
631                  }
632                  /* add to list */
633                  localControllers.add(controller);
634              } catch (ClassNotFoundException e) {
635                  throw new LifecycleException("Controller '"
636                          + controllerElement.getAttributeValue("class")
637                          + "' in bundle '" + this.name + "' cannot be found.", e);
638              } catch (InstantiationException e) {
639                  throw new LifecycleException("Controller '"
640                          + controllerElement.getAttributeValue("class")
641                          + "' in bundle '" + this.name
642                          + "' cannot be instantiated.", e);
643              } catch (IllegalAccessException e) {
644                  throw new LifecycleException("Illegal access to '"
645                          + controllerElement.getAttributeValue("class")
646                          + "' in bundle '" + this.name + "'.", e);
647              }
648          }
649          return localControllers;
650      }
651  
652      /**
653       * initialises the {@code Content} mapping.
654       */
655      private void initialiseContentMappings() {
656          Element contentMappings = this.bundleXML.getChild("content-mappings");
657          if (contentMappings != null) {
658              for (Object m : contentMappings.getChildren("mapping")) {
659                  Element mapping = (Element) m;
660  
661                  try {
662                      Class<? extends Object> contentClass = Class.forName(
663                              mapping.getAttributeValue("class")).asSubclass(
664                              Object.class);
665                      this.contentTypes.add(contentClass);
666                      ViewTypes viewTypes = new ViewTypes();
667                      for (Object vtO : mapping.getChildren("view")) {
668                          Element vt = (Element) vtO;
669                          String type = vt.getAttributeValue("type");
670                          String className = vt.getAttributeValue("class");
671                          View view = Class.forName(className).asSubclass(
672                                  View.class).newInstance();
673                          view.setName(vt.getAttributeValue("name"));
674                          CommandBuilder builder = new CommandBuilder(vt
675                                  .getChild("command"), this.getName());
676                          view.setCommandBuilder(builder);
677  
678                          /* set by type */
679                          initViewTypes(viewTypes, type, view);
680  
681                      }
682                      this.contentViews.put(contentClass, viewTypes);
683                  } catch (ClassNotFoundException e) {
684                      throw new LifecycleException("Bundle initialisation: "
685                              + e.getLocalizedMessage(), e);
686                  } catch (InstantiationException e) {
687                      throw new LifecycleException("Bundle initialisation: "
688                              + e.getLocalizedMessage(), e);
689                  } catch (IllegalAccessException e) {
690                      throw new LifecycleException("Bundle initialisation: "
691                              + e.getLocalizedMessage(), e);
692                  }
693              }
694          }
695      }
696  
697      /**
698       * initialises the view types.
699       * 
700       * @param viewTypes
701       *            the {@code ViewTypes} to be initialised.
702       * @param type
703       *            the type to check
704       * @param view
705       *            the view to set
706       */
707      private void initViewTypes(final ViewTypes viewTypes, final String type,
708              final View view) {
709          if (type.equals("default")) {
710              viewTypes.setDefaultView(view);
711          } else if (type.equals("create")) {
712              viewTypes.setCreateView(view);
713          } else if (type.equals("save")) {
714              viewTypes.setSaveView(view);
715          } else if (type.equals("edit")) {
716              viewTypes.setEditView(view);
717          } else if (type.equals("move")) {
718              viewTypes.setMoveView(view);
719          } else if (type.equals("delete")) {
720              viewTypes.setDeleteView(view);
721          } else if (type.equals("nodeCreateContent")) {
722              // special view for tree
723              viewTypes.setNodeCreateContentView(view);
724          } else if (type.equals("expandEdit")) {
725              // special view for tree (contentregistry editing)
726              viewTypes.setExpandEditView(view);
727          } else if (type.equals("expand")) {
728              // special view for tree (contentregistry selecting)
729              viewTypes.setExpandView(view);
730          }
731          // else if (type.equals("rename")) {
732          // // rename
733          // viewTypes.setRenameView(view);
734          // }
735          else {
736              viewTypes.addStandardView(view);
737          }
738      }
739  
740  
741      /**
742       * Returns a <tt>JSON</tt>-representation of the {@code Bundle}.
743       * 
744       * @return a <tt>JSON</tt>-representation of the {@code Bundle}
745       */
746      public JSONObject toJSON() {
747          JSONObject bundle = new JSONObject();
748          bundle.put("id", getId());
749          bundle.put("name", getName());
750          return bundle;
751      }
752  
753  }
754