1    /*
2     * Copyright 2006 :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.site.map;
19   
20   import java.io.Serializable;
21   import java.util.Date;
22   import java.util.HashMap;
23   import java.util.HashSet;
24   import java.util.List;
25   import java.util.Locale;
26   import java.util.Map;
27   import java.util.Set;
28   
29   import javax.persistence.Basic;
30   import javax.persistence.CascadeType;
31   import javax.persistence.CollectionTable;
32   import javax.persistence.Column;
33   import javax.persistence.ElementCollection;
34   import javax.persistence.Entity;
35   import javax.persistence.EnumType;
36   import javax.persistence.Enumerated;
37   import javax.persistence.FetchType;
38   import javax.persistence.JoinColumn;
39   import javax.persistence.ManyToMany;
40   import javax.persistence.OneToOne;
41   import javax.xml.bind.JAXBException;
42   import javax.xml.bind.annotation.XmlAccessOrder;
43   import javax.xml.bind.annotation.XmlAccessType;
44   import javax.xml.bind.annotation.XmlAccessorOrder;
45   import javax.xml.bind.annotation.XmlAccessorType;
46   import javax.xml.bind.annotation.XmlElement;
47   import javax.xml.bind.annotation.XmlElementWrapper;
48   import javax.xml.bind.annotation.XmlRootElement;
49   import javax.xml.bind.annotation.XmlTransient;
50   import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
51   
52   import net.sf.json.JSONObject;
53   
54   import org.hibernate.LazyInitializationException;
55   import org.hibernate.annotations.BatchSize;
56   import org.hibernate.search.annotations.Field;
57   import org.hibernate.search.annotations.FieldBridge;
58   import org.hibernate.search.annotations.FilterCacheModeType;
59   import org.hibernate.search.annotations.FullTextFilterDef;
60   import org.hibernate.search.annotations.FullTextFilterDefs;
61   import org.hibernate.search.annotations.Index;
62   import org.hibernate.search.annotations.Indexed;
63   import org.hibernate.search.annotations.IndexedEmbedded;
64   import org.hibernate.search.annotations.Store;
65   import org.jdom.Element;
66   import org.jdom.input.SAXHandler;
67   import org.slf4j.Logger;
68   import org.slf4j.LoggerFactory;
69   import org.torweg.pulse.accesscontrol.AbstractAccessControlObject;
70   import org.torweg.pulse.accesscontrol.Role;
71   import org.torweg.pulse.bundle.JDOMable;
72   import org.torweg.pulse.invocation.lifecycle.Lifecycle;
73   import org.torweg.pulse.service.PulseException;
74   import org.torweg.pulse.service.request.Command;
75   import org.torweg.pulse.site.LocaleMatchingException;
76   import org.torweg.pulse.site.View;
77   import org.torweg.pulse.util.INameable;
78   import org.torweg.pulse.util.entity.Node;
79   import org.torweg.pulse.util.search.LocaleFieldBridge;
80   import org.torweg.pulse.util.search.SitemapNodeDurationFilterFactory;
81   import org.torweg.pulse.util.search.SitemapNodeRoleFilterFactory;
82   import org.torweg.pulse.util.search.SitemapNodeSectionTagFilterFactory;
83   import org.torweg.pulse.util.search.SitemapNodeUniqueContentFilter;
84   import org.torweg.pulse.util.search.ViewFieldBridge;
85   import org.torweg.pulse.util.time.Duration;
86   import org.torweg.pulse.util.xml.bind.LocaleXmlAdapter;
87   
88   /**
89    * a {@code SitemapNode} is an entry in the websites {@code Sitemap}.
90    * <p>
91    * The {@code View} of the {@code SitemapNode} represents the association of the
92    * {@code SitemapNode} with a specific {@code Content}. Moreover, the
93    * {@code SitemapNode} may contain a set of attributes which may be used during
94    * XSL transformation or {@code OutputEvent}s to customize the output.
95    * </p>
96    * 
97    * @author Thomas Weber, Daniel Dietz
98    * @version $Revision: 1822 $
99    * @see Sitemap
100   * @see View
101   * @see SitemapDisabledActions
102   * @see SitemapNodeUniqueContentFilter
103   */
104  @Entity
105  @Indexed
106  @FullTextFilterDefs({
107          @FullTextFilterDef(name = "uniqueContents", impl = SitemapNodeUniqueContentFilter.class, cache = FilterCacheModeType.INSTANCE_AND_DOCIDSETRESULTS),
108          @FullTextFilterDef(name = "inRoles", impl = SitemapNodeRoleFilterFactory.class, cache = FilterCacheModeType.INSTANCE_AND_DOCIDSETRESULTS),
109          @FullTextFilterDef(name = "inSection", impl = SitemapNodeSectionTagFilterFactory.class, cache = FilterCacheModeType.INSTANCE_AND_DOCIDSETRESULTS),
110          @FullTextFilterDef(name = "duration", impl = SitemapNodeDurationFilterFactory.class, cache = FilterCacheModeType.INSTANCE_AND_DOCIDSETRESULTS) })
111  @XmlRootElement(name = "sitemapnode")
112  @XmlAccessorOrder(XmlAccessOrder.UNDEFINED)
113  @XmlAccessorType(XmlAccessType.FIELD)
114  public class SitemapNode extends Node implements INameable, JDOMable,
115          Serializable {
116  
117      /**
118       * The serialVersionUID.
119       */
120      private static final long serialVersionUID = 2785382406241489630L;
121  
122      /**
123       * the logger.
124       */
125      private static final Logger LOGGER = LoggerFactory
126              .getLogger(SitemapNode.class);
127  
128      /**
129       * the Locale of the SitemapNode.
130       */
131      @Basic
132      @Column(nullable = false)
133      @Field(name = "locale", index = Index.UN_TOKENIZED, store = Store.YES)
134      @FieldBridge(impl = LocaleFieldBridge.class)
135      @XmlJavaTypeAdapter(value = LocaleXmlAdapter.class)
136      private Locale locale;
137  
138      /**
139       * the name of the {@code SitemapNode}.
140       */
141      @Basic
142      @Column(nullable = false)
143      @XmlElement(name = "name")
144      private String name;
145  
146      /**
147       * The type of the {@code SitemapNode}.
148       * <p>
149       * default: <tt>DEFAULT</tt>
150       * <p>
151       */
152      @Enumerated(EnumType.STRING)
153      @Column(nullable = false)
154      @XmlElement(name = "sitemapnode-type")
155      private SitemapNode.Type type = SitemapNode.Type.DEFAULT;
156  
157      /**
158       * indicator, whether the {@code SitemapNode} is visible.
159       */
160      @Basic
161      @Column(nullable = false)
162      @XmlElement(name = "visible")
163      private boolean visible = false;
164  
165      /**
166       * the {@code View} associated with the {@code SitemapNode}.
167       */
168      @OneToOne(cascade = CascadeType.ALL)
169      @JoinColumn(name = "view_id")
170      @Field(name = "fulltext", index = Index.TOKENIZED, store = Store.YES)
171      @FieldBridge(impl = ViewFieldBridge.class)
172      @XmlTransient
173      // getter JAXB-annotated
174      private View view;
175  
176      /**
177       * the id of the content contained in {@code view} or {@code null} , if the
178       * {@code View} does not point to a {@code Content}.
179       * <p>
180       * The value is maintained by
181       * {@link org.torweg.pulse.util.search.SitemapNodeInterceptor} and used by
182       * the FullTextFilter to provide unique contents.
183       * </p>
184       */
185      @Basic
186      @Field(name = "contentId", index = Index.UN_TOKENIZED, store = Store.YES)
187      @XmlElement(name = "content-id")
188      private Long contentId;
189  
190      /**
191       * the attributes associated with the {@code SitemapNode}.
192       */
193      @ElementCollection
194      @CollectionTable(name = "SitemapNode_attributes", joinColumns = @JoinColumn(name = "SitemapNode_id"))
195      @BatchSize(size = 20)
196      @XmlTransient
197      // getter JAXB-annotated
198      private Map<String, String> attributes = new HashMap<String, String>();
199  
200      /**
201       * The {@code Role}s associated with the {@code SitemapNode}.
202       */
203      @IndexedEmbedded
204      @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE,
205              CascadeType.REFRESH }, fetch = FetchType.EAGER)
206      @BatchSize(size = 50)
207      @XmlTransient
208      // getter JAXB-annotated
209      private final Set<Role> roles = new HashSet<Role>();
210  
211      /**
212       * the {@code SitemapSectionTag} associated with the {@code SitemapNode}.
213       */
214      @IndexedEmbedded
215      @OneToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST,
216              CascadeType.REFRESH }, fetch = FetchType.EAGER)
217      @JoinColumn(name = "sectionTag_id")
218      @XmlElement(name = "sitemap-section-tag")
219      private SitemapSectionTag sectionTag = null;
220  
221      /**
222       * the start date for the validity.
223       */
224      @Basic(optional = true)
225      @Field(name = "validity_startDate", index = Index.UN_TOKENIZED, store = Store.YES)
226      @XmlElement(name = "start-date")
227      private Long startDate;
228  
229      /**
230       * the end date for the validity.
231       */
232      @Basic(optional = true)
233      @Field(name = "validity_endDate", index = Index.UN_TOKENIZED, store = Store.YES)
234      @XmlElement(name = "end-date")
235      private Long endDate;
236  
237      /**
238       * protected constructor for Hibernate<sup>TM</sup>.
239       */
240      @Deprecated
241      protected SitemapNode() {
242          super();
243      }
244  
245      /**
246       * creates a new {@code SitemapNode} with the given name and {@code Locale}.
247       * 
248       * @param n
249       *            the name
250       * @param loc
251       *            the {@code Locale}
252       */
253      public SitemapNode(final String n, final Locale loc) {
254          super();
255          this.name = n;
256          this.locale = loc;
257      }
258  
259      /**
260       * creates a new {@code SitemapNode} with the given name, {@code Locale} and
261       * {@code Type}.
262       * 
263       * @param n
264       *            the name
265       * @param loc
266       *            the {@code Locale}
267       * @param t
268       *            the {@code Type}
269       */
270      public SitemapNode(final String n, final Locale loc, final Type t) {
271          super();
272          this.name = n;
273          this.locale = loc;
274          this.type = t;
275      }
276  
277      /**
278       * creates a new {@code SitemapNode} with the given name, {@code Locale} and
279       * {@code View}.
280       * 
281       * @param n
282       *            the name
283       * @param loc
284       *            the {@code Locale}
285       * @param v
286       *            the {@code View} to be associated with the {@code SitemapNode}
287       */
288      public SitemapNode(final String n, final Locale loc, final View v) {
289          super();
290          this.name = n;
291          this.locale = loc;
292          this.view = v;
293      }
294  
295      /**
296       * creates a new {@code SitemapNode} with the given name, {@code Locale},
297       * {@code View} and {@code Type}.
298       * 
299       * @param n
300       *            the name
301       * @param loc
302       *            the {@code Locale}
303       * @param v
304       *            the {@code View} to be associated with the {@code SitemapNode}
305       * @param t
306       *            the {@code Type}
307       */
308      public SitemapNode(final String n, final Locale loc, final View v,
309              final Type t) {
310          super();
311          this.name = n;
312          this.locale = loc;
313          this.view = v;
314          this.type = t;
315      }
316  
317      /**
318       * sets the {@code Locale} of the {@code SitemapNode}.
319       * 
320       * @param loc
321       *            the {@code Locale} to be set
322       */
323      public final void setLocale(final Locale loc) {
324          this.locale = loc;
325      }
326  
327      /**
328       * @return returns the {@code Locale} of the {@code SitemapNode}
329       */
330      public final Locale getLocale() {
331          return this.locale;
332      }
333  
334      /**
335       * sets a new name for the {@code SitemapNode}.
336       * 
337       * @param n
338       *            the name to be set
339       */
340      public final void setName(final String n) {
341          this.name = n;
342      }
343  
344      /**
345       * @return returns the name of the {@code SitemapNode}.
346       */
347      public final String getName() {
348          return this.name;
349      }
350  
351      /**
352       * Sets the {@code Type} for the {@code SitemapNode}.
353       * 
354       * @param t
355       *            the <tt>type</tt> to set
356       */
357      public final void setType(final Type t) {
358          this.type = t;
359      }
360  
361      /**
362       * Returns the {@code Type} of the {@code SitemapNode}.
363       * 
364       * @return the <tt>type</tt>
365       */
366      public final Type getType() {
367          return type;
368      }
369  
370      /**
371       * Adds a new {@code SitemapNode} to the children of this
372       * {@code SitemapNode} if {@code Locale}s match.
373       * 
374       * @param n
375       *            the {@code SitemapNode} to be added.
376       */
377      @Override
378      public void addChild(final Node n) {
379          SitemapNode tn = (SitemapNode) n;
380          if (!tn.locale.equals(this.locale)) {
381              throw new LocaleMatchingException("Cannot add child! Locale "
382                      + tn.locale.toString() + " does not match "
383                      + this.locale.toString() + ".");
384          }
385          super.addChild(tn);
386      }
387  
388      /**
389       * adds a new {@code SitemapNode} at a given position to the list children
390       * of this {@code SitemapNode} if {@code Locale}s match.
391       * 
392       * @param pos
393       *            the position in the list of children
394       * @param n
395       *            the {@code SitemapNode} to become a child of this
396       *            {@code SitemapNode}.
397       */
398      @Override
399      public void addChild(final int pos, final Node n) {
400          SitemapNode tn = (SitemapNode) n;
401          if (!tn.locale.equals(this.locale)) {
402              throw new LocaleMatchingException("Cannot add child! Locale "
403                      + tn.locale.toString() + " does not match "
404                      + this.locale.toString() + ".");
405          }
406          super.addChild(pos, tn);
407      }
408  
409      /**
410       * sets the children of the {@code SitemapNode} if {@code Locale}s match.
411       * 
412       * @param nodeList
413       *            the list of child {@code SitemapNode}s
414       * @param <E>
415       *            a class extending {@code Node}
416       */
417      @Override
418      public <E extends Node> void setChildren(final List<E> nodeList) {
419          for (Node n : nodeList) {
420              SitemapNode tn = (SitemapNode) n;
421              if (!tn.locale.equals(this.locale)) {
422                  throw new LocaleMatchingException("Cannot add child! Locale "
423                          + tn.locale.toString() + " does not match "
424                          + this.locale.toString() + ".");
425              }
426          }
427          super.setChildren(nodeList);
428      }
429  
430      /**
431       * @return Returns the attributes.
432       */
433      @XmlTransient
434      public Map<String, String> getAttributes() {
435          return attributes;
436      }
437  
438      /**
439       * For JAXB only.
440       * 
441       * @return this.getAttributes()
442       */
443      @XmlElementWrapper(name = "attributes")
444      @XmlElement(name = "attribute")
445      @SuppressWarnings("unused")
446      @Deprecated
447      private HashSet<Attribute> getAttributesJAXB() { // NOPMD
448          try {
449              return new HashSet<Attribute>(
450                      Attribute.getAttributes(getAttributes()));
451          } catch (LazyInitializationException e) {
452              LOGGER.debug("ignored: {}", e.getLocalizedMessage());
453              return null;
454          }
455      }
456  
457      /**
458       * @param att
459       *            The attributes to set.
460       */
461      public void setAttributes(final Map<String, String> att) {
462          this.attributes = att;
463      }
464  
465      /**
466       * adds an attribute to the {@code SitemapNode}.
467       * 
468       * @param key
469       *            the key
470       * @param value
471       *            the value
472       */
473      public void putAttribute(final String key, final String value) {
474          this.attributes.put(key, value);
475      }
476  
477      /**
478       * removes an attribute from the {@code SitemapNode}.
479       * 
480       * @param key
481       *            the key of the attribute to be removed
482       * @return {@code true}, if the attribute could be removed. Otherwise
483       *         {@code false}.
484       */
485      public boolean removeAttribute(final String key) {
486          String removed = this.attributes.remove(key);
487          if (removed == null) {
488              return false;
489          }
490          return true;
491      }
492  
493      /**
494       * @return Returns the view.
495       */
496      public final View getView() {
497          return view;
498      }
499  
500      /**
501       * For JAXB only.
502       * 
503       * @return this.getView()
504       */
505      @XmlElement(name = "view")
506      @SuppressWarnings("unused")
507      @Deprecated
508      private View getViewJAXB() {
509          try {
510              return getView();
511          } catch (LazyInitializationException e) {
512              LOGGER.debug("ignored: {}", e.getLocalizedMessage());
513              return null;
514          }
515      }
516  
517      /**
518       * @param v
519       *            The view to set.
520       */
521      public final void setView(final View v) {
522          this.view = v;
523      }
524  
525      /**
526       * removes the {@code View} from the {@code SitemapNode}.
527       */
528      public final void removeView() {
529          this.view = null; // NOPMD by thomas on 29.02.08 21:51
530      }
531  
532      /**
533       * returns the id of the {@code Content} referenced by the
534       * {@code SitemapNode}'s {@code View}.
535       * 
536       * @return the id of the {@code Content} or {@code null}
537       */
538      public final Long getContentId() {
539          return this.contentId;
540      }
541  
542      /**
543       * returns the {@code Command} that produces the view associated with the
544       * {@code SitemapNode}.
545       * <p>
546       * If the {@code View} is {@code null}, the returned {@code Command} is the
547       * template with an updated {@code SitemapNodeID} that matches the ID of the
548       * {@code SitemapNode}.
549       * </p>
550       * 
551       * @param template
552       *            the {@code Command} template
553       * @return the {@code Command} that produces the view associated with the
554       *         {@code SitemapNode}.
555       */
556      public Command getCommand(final Command template) {
557          if (this.view != null) {
558              return this.view.getCommand(template);
559          } else {
560              // special treatment of grouping-nodes
561              if (this.visible && (getChildren().size() > 0)
562                      && (this.getParent() != null)) {
563                  // returns the command template with an updated
564                  // SitemapNodeID and action of first parent node where view !=
565                  // null
566                  SitemapNode sn = (SitemapNode) this.getParent();
567                  while (sn.getView() == null) {
568                      sn = (SitemapNode) sn.getParent();
569                  }
570                  return sn.getView().getCommand(template);
571              }
572          }
573  
574          /* returns the command template with an updated SitemapNodeID */
575          return template.newInstance(template.getLocale(), template.getBundle(),
576                  template.getAction(), template.getParameters()).setSitemapID(
577                  getId());
578      }
579  
580      /**
581       * @return Returns {@code true}, if the {@code SitemapNode} is visible.
582       */
583      public final boolean isVisible() {
584          return this.visible;
585      }
586  
587      /**
588       * sets the visibility of the {@code SitemapNode}.
589       * 
590       * @param v
591       *            {@code true} to make the {@code SitemapNode} visible,
592       *            {@code false} to hide the {@code SitemapNode}.
593       */
594      public final void setVisible(final boolean v) {
595          this.visible = v;
596      }
597  
598      /**
599       * Returns a shallow copy of the {@code SitemapNode}'s {@code Collection} of
600       * {@code Role}s.
601       * 
602       * <p>
603       * Attention: Since the returned {@code Set} is not a reference to the
604       * {@code SitemapNode}'s internal {@code Collection}, any modifications to
605       * its state will get lost. However, modifications to the state of the
606       * {@code Set}'s elements will be consistent.
607       * </p>
608       * 
609       * <p>
610       * For example: Adding/Deleting a {@code Role} to/from the {@code Set} will
611       * not have any effect, when the {@code SitemapNode} is saved. On the other
612       * hand, modifications to a {@code Role} in the {@code Set} will be made
613       * persistent, when the {@code Role} is saved.
614       * </p>
615       * 
616       * @return the {@code Role}s associated with the {@code User}
617       */
618      public final Set<Role> getRoles() {
619          return new HashSet<Role>(this.roles);
620      }
621  
622      /**
623       * For JAXB only.
624       * 
625       * @return this.getRoles()
626       */
627      @XmlElementWrapper(name = "roles")
628      @XmlElement(name = "role")
629      @SuppressWarnings("unused")
630      @Deprecated
631      private HashSet<Role> getRolesJAXB() { // NOPMD
632          try {
633              return new HashSet<Role>(getRoles());
634          } catch (LazyInitializationException e) {
635              LOGGER.debug("ignored: {}", e.getLocalizedMessage());
636              return null;
637          }
638      }
639  
640      /**
641       * Adds a {@code Role} to the {@code SitemapNode}'s {@code Set} of
642       * {@code Role}s. If it already is part of the {@code Set}, it will not be
643       * added.
644       * 
645       * @param rl
646       *            the {@code Role} to be added
647       * @return true if the {@code Set} changed as a result of the call
648       */
649      public final boolean addRole(final Role rl) {
650          return this.roles.add(rl);
651      }
652  
653      /**
654       * Removes a {@code Role} from the {@code SitemapNode}'s {@code Set} of
655       * {@code Role}s if it is part of it.
656       * 
657       * @param rl
658       *            the {@code Role} to be removed
659       * @return true if the {@code Set} changed as a result of the call
660       */
661      public final boolean removeRole(final AbstractAccessControlObject rl) {
662          return this.roles.remove(rl);
663      }
664  
665      /**
666       * Removes all roles form the {@code SitemapNode}.
667       */
668      public final void clearRoles() {
669          this.roles.clear();
670      }
671  
672      /**
673       * Sets the {@code SitemapSectionTag} for the {@code SitemapNode}.
674       * 
675       * @param tag
676       *            the <tt>sectionTag</tt> to set
677       */
678      public final void setSectionTag(final SitemapSectionTag tag) {
679          this.sectionTag = tag;
680      }
681  
682      /**
683       * Returns the {@code SitemapSectionTag} of the {@code SitemapNode}.
684       * 
685       * @return the <tt>sectionTag</tt>
686       */
687      public final SitemapSectionTag getSectionTag() {
688          return this.sectionTag;
689      }
690  
691      /**
692       * sets the {@code Duration} for the {@code SitemapNode}.
693       * 
694       * @param validity
695       *            the validity
696       */
697      public final void setDuration(final Duration validity) {
698          this.startDate = validity.getStartDate().getTime();
699          this.endDate = validity.getEndDate().getTime();
700      }
701  
702      /**
703       * returns the {@code Duration} of the {@code SitemapNode} or {@code null}
704       * if no duration has been set.
705       * 
706       * @return the validity or {@code null}
707       */
708      public final Duration getDuration() {
709          if ((this.startDate != null) && (this.endDate != null)) {
710              return new Duration(new Date(this.startDate),
711                      new Date(this.endDate));
712          }
713          return null;
714      }
715  
716      /**
717       * removes the {@code Duration} from the {@code SitemapNode}.
718       */
719      public final void removeDuration() {
720          this.endDate = null; // NOPMD
721          this.startDate = null; // NOPMD
722      }
723  
724      /**
725       * Adopts the following settings from the given {@code SitemapNode}:
726       * <ul>
727       * <li>roles (are being overridden)</li>
728       * <li>visibility</li>
729       * <li>sectionTag</li>
730       * <li>attributes (are being overridden)</li>
731       * <li>duration (startDate/endDate)</li>
732       * </ul>
733       * .
734       * 
735       * @param sitemapNode
736       *            the {@code SitemapNode} to adopt the settings from
737       */
738      public final void adoptSettingsFrom(final SitemapNode sitemapNode) {
739  
740          // roles
741          this.roles.clear();
742          for (Role rl : sitemapNode.getRoles()) {
743              this.addRole(rl);
744          }
745  
746          // visibility
747          this.setVisible(sitemapNode.isVisible());
748  
749          // section-tag
750          this.setSectionTag(sitemapNode.getSectionTag());
751  
752          // attributes
753          this.attributes.clear();
754          for (Map.Entry<String, String> entry : sitemapNode.getAttributes()
755                  .entrySet()) {
756              this.putAttribute(entry.getKey(), entry.getValue());
757          }
758  
759          // start-/end-date
760          if (sitemapNode.getDuration() != null) {
761              this.setDuration(sitemapNode.getDuration());
762          }
763  
764      }
765  
766      /**
767       * Returns a {@code StringBuilder} with the {@code Sitemap}-path of the
768       * {@code SitemapNode}. <tt>System.getProperty("file.separator")</tt> will
769       * be used as path-separator.
770       * 
771       * @return a {@code StringBuilder} with the {@code Sitemap}-path of the
772       *         {@code SitemapNode}
773       */
774      public final StringBuilder getPathBuilder() {
775          if (getParent() != null) {
776              return ((SitemapNode) getParent()).getPathBuilder()
777                      .append(System.getProperty("file.separator"))
778                      .append(getName());
779          }
780          return new StringBuilder(getName());
781      }
782  
783      /**
784       * @return the {@code SitemapNode} as an {@code Element}
785       */
786      public final Element deserializeToJDOM() {
787          Element node = deserializeToJDOMShallow();
788          if (getView() != null) {
789              node.addContent(getView().deserializeToJDOM());
790          }
791          try {
792              List<Node> children = getChildren();
793              for (Node c : children) {
794                  SitemapNode child = (SitemapNode) c;
795                  node.addContent(child.deserializeToJDOM());
796              }
797              return node;
798          } catch (LazyInitializationException e) {
799              // no children
800              return node;
801          }
802      }
803  
804      /**
805       * returns the {@code SitemapNode} as an {@code Element} excluding all
806       * children and view information.
807       * 
808       * @return the {@code SitemapNode} as an {@code Element} excluding all
809       *         children and view information
810       */
811      public final Element deserializeToJDOMShallow() {
812          Element node = new Element("SitemapNode");
813          node.setAttribute("id", getId().toString());
814          node.setAttribute("class", getClass().getCanonicalName());
815          node.setAttribute("name", getName());
816          node.setAttribute("locale", getLocale().toString());
817          node.setAttribute("visible", Boolean.toString(isVisible()));
818          // add the attributes of the sitemapNode
819          Element nodeAttrs = new Element("attributes");
820          try {
821              for (Map.Entry<String, String> m : getAttributes().entrySet()) {
822                  nodeAttrs.addContent(new Element("key").setAttribute("name",
823                          m.getKey()).setText(m.getValue()));
824              }
825          } catch (LazyInitializationException e) { // NOPMD CHECKSTYLE:OFF
826              // CHECKSTYLE:ON
827          }
828          node.addContent(nodeAttrs);
829  
830          if (getSectionTag() != null) {
831              node.addContent(getSectionTag().deserializeToJDOM());
832          }
833          if (getDuration() != null) {
834              // node.addContent(getDuration().deserializeToJDOM());
835              SAXHandler jdomSaxHandler = new SAXHandler();
836              try {
837                  Lifecycle.getJAXBContext().createMarshaller()
838                          .marshal(getDuration(), jdomSaxHandler);
839              } catch (JAXBException e) {
840                  throw new PulseException("Error: " + e.getLocalizedMessage(), e);
841              }
842              node.addContent(jdomSaxHandler.getDocument().detachRootElement());
843          }
844          return node;
845      }
846  
847      /**
848       * /** returns a {@code JSONObject} representation of the
849       * {@code SitemapNode}.
850       * 
851       * @return a {@code JSONObject} representation of the {@code SitemapNode}
852       */
853      @Override
854      public final JSONObject toJSON() {
855          JSONObject nodeObject = super.toJSON();
856          // nodeObject.remove("leaf");
857          nodeObject.put("text", getName());
858          nodeObject.put("uiProvider", "SitemapTreeNodeUI");
859          nodeObject.put("locale", getLocale().toString());
860          nodeObject.put("type", getType());
861  
862          // nodeObject.put("allowChildren", true);
863          // nodeObject.put("allowDrop", true);
864          nodeObject.put("visible", Boolean.toString(isVisible()));
865          if (this.view != null) {
866              nodeObject.put("viewId", this.view.getId().toString());
867              nodeObject.put("contentId", this.view.getContent().getId()
868                      .toString());
869              nodeObject.put("contentType", this.view.getContent().getClass()
870                      .getCanonicalName());
871              nodeObject.put("contentName", this.view.getContent().getName());
872              nodeObject.put("contentLocale", this.view.getContent().getLocale()
873                      .toString());
874              nodeObject.put("contentBundle", this.view.getContent().getBundle()
875                      .getName());
876          }
877          if (this.sectionTag != null) {
878              nodeObject.put("sectionTagId", this.sectionTag.getId());
879              nodeObject.put("sectionTagName", this.sectionTag.getName());
880          }
881  
882          return nodeObject;
883      }
884  
885      /**
886       * The different {@code Type}s available for {@code SitemapNode}s.
887       * <ul>
888       * <li><tt>DEFAULT</tt></li>
889       * <li><tt>VIRTUAL</tt></li>
890       * </ul>
891       * 
892       * @author Daniel Dietz
893       * @version $Revision: 1822 $
894       * 
895       */
896      public static enum Type {
897  
898          /**
899           * The default {@code Type} for {@code SitemapNode}s if not specified
900           * otherwise.
901           */
902          DEFAULT,
903  
904          /**
905           * Indicates a virtual {@code SitemapNode}.
906           */
907          VIRTUAL
908  
909      }
910  
911      /**
912       * Utility-class for outputting an {@code Attribute} via JAXB.
913       * 
914       * @author Daniel Dietz
915       * @version $Revision: 1822 $
916       * 
917       */
918      @XmlRootElement(name = "attribute")
919      @XmlAccessorOrder(XmlAccessOrder.UNDEFINED)
920      @XmlAccessorType(XmlAccessType.FIELD)
921      private static final class Attribute implements Serializable {
922  
923          /**
924           * The serialVersionUID.
925           */
926          private static final long serialVersionUID = -4681409554026843115L;
927  
928          /**
929           * The key.
930           */
931          @XmlElement(name = "key")
932          @SuppressWarnings("unused")
933          private String key; // NOPMD
934  
935          /**
936           * The value.
937           */
938          @XmlElement(name = "value")
939          @SuppressWarnings("unused")
940          private String value; // NOPMD
941  
942          /**
943           * Default constructor for JAXB.
944           */
945          @Deprecated
946          @SuppressWarnings("unused")
947          protected Attribute() {
948              super();
949          }
950  
951          /**
952           * Creates a new {@code Attribute} with the given key and the given
953           * value.
954           * 
955           * @param k
956           *            the key
957           * @param v
958           *            the value
959           */
960          protected Attribute(final String k, final String v) {
961              super();
962              this.key = k;
963              this.value = v;
964          }
965  
966          /**
967           * Convenience method to create a {@code Set<Attribute>} from a given
968           * {@code Map<String, String>}.
969           * 
970           * @param attributes
971           *            the {@code Map<String, String>} of attributes
972           * 
973           * @return a {@code Set<Attribute>}
974           */
975          protected static Set<Attribute> getAttributes(
976                  final Map<String, String> attributes) {
977              Set<Attribute> attrs = new HashSet<Attribute>();
978              for (Map.Entry<String, String> entry : attributes.entrySet()) {
979                  attrs.add(new Attribute(entry.getKey(), entry.getValue()));
980              }
981              return attrs;
982          }
983  
984      }
985  
986  }
987