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.content;
19   
20   import java.io.Serializable;
21   import java.net.URI;
22   import java.net.URISyntaxException;
23   import java.text.Collator;
24   import java.util.Collection;
25   import java.util.Date;
26   import java.util.HashSet;
27   import java.util.Locale;
28   import java.util.Set;
29   
30   import javax.persistence.Basic;
31   import javax.persistence.CascadeType;
32   import javax.persistence.Column;
33   import javax.persistence.Entity;
34   import javax.persistence.FetchType;
35   import javax.persistence.Inheritance;
36   import javax.persistence.InheritanceType;
37   import javax.persistence.JoinColumn;
38   import javax.persistence.JoinTable;
39   import javax.persistence.ManyToMany;
40   import javax.persistence.ManyToOne;
41   import javax.persistence.OneToMany;
42   import javax.persistence.Temporal;
43   import javax.persistence.TemporalType;
44   import javax.xml.bind.annotation.XmlAccessOrder;
45   import javax.xml.bind.annotation.XmlAccessType;
46   import javax.xml.bind.annotation.XmlAccessorOrder;
47   import javax.xml.bind.annotation.XmlAccessorType;
48   import javax.xml.bind.annotation.XmlAttribute;
49   import javax.xml.bind.annotation.XmlElement;
50   import javax.xml.bind.annotation.XmlElementWrapper;
51   import javax.xml.bind.annotation.XmlRootElement;
52   import javax.xml.bind.annotation.XmlTransient;
53   import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
54   
55   import org.hibernate.LazyInitializationException;
56   import org.hibernate.annotations.BatchSize;
57   import org.jdom.Element;
58   import org.slf4j.Logger;
59   import org.slf4j.LoggerFactory;
60   import org.torweg.pulse.accesscontrol.User;
61   import org.torweg.pulse.bundle.Bundle;
62   import org.torweg.pulse.bundle.JDOMable;
63   import org.torweg.pulse.service.PulseException;
64   import org.torweg.pulse.service.request.CommandBuilder;
65   import org.torweg.pulse.site.View;
66   import org.torweg.pulse.site.ViewTypes;
67   import org.torweg.pulse.site.content.util.ILinkCorretable;
68   import org.torweg.pulse.util.INameable;
69   import org.torweg.pulse.util.entity.AbstractImageGroup;
70   import org.torweg.pulse.util.entity.AbstractNamableEntity;
71   import org.torweg.pulse.util.time.Duration;
72   import org.torweg.pulse.util.xml.bind.LocaleXmlAdapter;
73   import org.torweg.pulse.vfs.VirtualFile;
74   import org.torweg.pulse.vfs.VirtualFileSystem;
75   
76   /**
77    * An abstract base class for {@code Content}s referenced by a {@code View} in
78    * the {@code Sitemap}.
79    * 
80    * @author Thomas Weber, Daniel Dietz, Christian Schatt
81    * @version $Revision: 1825 $
82    */
83   @Entity
84   @Inheritance(strategy = InheritanceType.JOINED)
85   @XmlRootElement(name = "content")
86   @XmlAccessorOrder(XmlAccessOrder.UNDEFINED)
87   @XmlAccessorType(XmlAccessType.FIELD)
88   public abstract class Content extends AbstractNamableEntity // NOPMD
89           implements INameable, ILinkCorretable, Comparable<Content>, JDOMable {
90   
91       /**
92        * serialVersionUID.
93        */
94       private static final long serialVersionUID = -2416753273274390014L;
95   
96       /**
97        * The {@code Logger} of the {@code Content}.
98        */
99       private static final Logger LOGGER = LoggerFactory.getLogger(Content.class);
100  
101      /**
102       * The {@code Locale} of the {@code Content}.
103       */
104      @Basic(optional = false)
105      @XmlJavaTypeAdapter(value = LocaleXmlAdapter.class)
106      private Locale locale = null;
107  
108      /**
109       * The {@code Bundle} of the {@code Content}.
110       */
111      @ManyToOne
112      @XmlElement(name = "bundle")
113      private Bundle bundle = null;
114  
115      /**
116       * The {@code ContentLocalizationMap} of the {@code Content}.
117       */
118      @ManyToOne(cascade = CascadeType.ALL, optional = false)
119      @BatchSize(size = 20)
120      @XmlTransient
121      // getter is JAXB-annotated
122      private ContentLocalizationMap localizationMap = null;
123  
124      /**
125       * The {@code View}s of the {@code Content}.
126       */
127      @OneToMany(mappedBy = "content")
128      @BatchSize(size = 20)
129      @XmlTransient
130      // getter is JAXB-annotated
131      private final Set<View> associatedViews = new HashSet<View>();
132  
133      /**
134       * The {@code VirtualFile}s of the {@code Content}.
135       */
136      @ManyToMany
137      @JoinTable(name = "Content_VirtualFile", joinColumns = { @JoinColumn(name = "content_id") }, inverseJoinColumns = { @JoinColumn(name = "virtualfile_id") })
138      @BatchSize(size = 20)
139      @XmlTransient
140      // getter is JAXB-annotated
141      private Set<VirtualFile> associatedVirtualFiles = new HashSet<VirtualFile>();
142  
143      /**
144       * The {@code Attachment}s of the {@code Content}.
145       */
146      @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
147      @BatchSize(size = 20)
148      @XmlTransient
149      // getter is JAXB-annotated
150      private Set<Attachment> attachments = new HashSet<Attachment>();
151  
152      /**
153       * The {@code User} which has created the {@code Content}.
154       */
155      @ManyToOne(optional = false)
156      @XmlElement(name = "creator")
157      private User creator = null;
158  
159      /**
160       * The {@code Date} of creation.
161       */
162      @Temporal(TemporalType.TIMESTAMP)
163      @Column(nullable = false)
164      @XmlElement(name = "created")
165      // also see getCreatedMillis
166      private Date created = null;
167  
168      /**
169       * The {@code User} which has last modified the {@code Content}.
170       */
171      @ManyToOne(optional = false)
172      @XmlElement(name = "last-modifier")
173      private User lastModifier = null;
174  
175      /**
176       * The {@code Date} of the last modification.
177       */
178      @Temporal(TemporalType.TIMESTAMP)
179      @Column(nullable = false)
180      @XmlElement(name = "last-modified")
181      private Date lastModified = null;
182  
183      /**
184       * start timestamp for reference duration.
185       */
186      @XmlTransient
187      // see getReferenceDuration()
188      @Basic(optional = true)
189      private Long referenceDurationStart;
190  
191      /**
192       * end timestamp for reference duration.
193       */
194      @XmlTransient
195      // see getReferenceDuration()
196      @Basic(optional = true)
197      private Long referenceDurationEnd;
198  
199      /**
200       * Returns the {@code User} which has created the {@code Content}.
201       * 
202       * @return the <tt>creator</tt>
203       */
204      public final User getCreator() {
205          return this.creator;
206      }
207  
208      /**
209       * Sets the {@code User} which has created the {@code Content}.
210       * <p>
211       * Does set the {@code Date} for <tt>created</tt>.
212       * </p>
213       * 
214       * @param user
215       *            the <tt>creator</tt> to set
216       */
217      public final void setCreator(final User user) {
218          if (this.creator != null) {
219              throw new PulseException("Cannot override creator { "
220                      + this.creator + " } for Content: { " + this + " }!");
221          }
222          if (user == null) {
223              throw new PulseException(
224                      "Cannot set creator to NULL for Content: { " + this + " }!");
225          }
226          this.creator = user;
227          setCreated();
228          setLastModifier(user);
229      }
230  
231      /**
232       * Returns the {@code Date} of the creation of the content.
233       * 
234       * @return the <tt>created</tt>
235       */
236      public final Date getCreated() {
237          return new Date(this.created.getTime());
238      }
239  
240      /**
241       * Returns the {@code Date}-millis of the creation of the content.
242       * <p>
243       * Usage: for JAXB
244       * </p>
245       * 
246       * @return the <tt>created</tt>-millis
247       */
248      @XmlElement(name = "created-millis")
249      public final Long getCreatedMillis() {
250          return this.created.getTime();
251      }
252  
253      /**
254       * Sets the creation-{@code Date} of the {@code Content} to the current
255       * {@code Date}.
256       */
257      private void setCreated() {
258          this.created = new Date();
259      }
260  
261      /**
262       * Returns the {@code User} which has last modified the {@code Content}.
263       * 
264       * @return the <tt>lastModifier</tt>
265       */
266      public final User getLastModifier() {
267          return this.lastModifier;
268      }
269  
270      /**
271       * Sets the {@code User} which has last modified the {@code Content}.
272       * <p>
273       * Does set the {@code Date} for <tt>lastModified</tt>.
274       * </p>
275       * 
276       * @param user
277       *            the {@code User} to set
278       */
279      public final void setLastModifier(final User user) {
280          if (user == null) {
281              throw new PulseException(
282                      "Cannot set last modifier to NULL for Content: { " + this
283                              + " }!");
284          }
285          this.lastModifier = user;
286          setLastModified();
287      }
288  
289      /**
290       * Returns the {@code Date} of the last modification.
291       * 
292       * @return the <tt>lastModified</tt>
293       */
294      public final Date getLastModified() {
295          return new Date(this.lastModified.getTime());
296      }
297  
298      /**
299       * Sets the {@code Date} of the last modification of {@code Content} to the
300       * current {@code Date}.
301       */
302      private void setLastModified() {
303          this.lastModified = new Date();
304      }
305  
306      /**
307       * Returns the {@code Locale} of the {@code Content}.
308       * 
309       * @return the {@code Locale} of the {@code Content}.
310       */
311      public final Locale getLocale() {
312          return this.locale;
313      }
314  
315      /**
316       * Sets the {@code Locale} of the {@code Content}.
317       * 
318       * @param pLocale
319       *            the {@code Locale} to be set.
320       */
321      public final void setLocale(final Locale pLocale) {
322          ContentLocalizationMap map = this.localizationMap;
323          if (map != null) {
324              map.remove(this.locale);
325              this.locale = pLocale;
326              map.put(this.locale, this);
327          } else {
328              this.locale = pLocale;
329              map = new ContentLocalizationMap(pLocale, this);
330          }
331          this.localizationMap = map;
332      }
333  
334      /**
335       * Returns the {@code Bundle} of the {@code Content}.
336       * 
337       * @return the {@code Bundle} of the {@code Content}.
338       */
339      public final Bundle getBundle() {
340          return this.bundle;
341      }
342  
343      /**
344       * Sets the {@code Bundle} of the {@code Content}.
345       * 
346       * @param pBundle
347       *            the {@code Bundle} to be set.
348       */
349      public final void setBundle(final Bundle pBundle) {
350          this.bundle = pBundle;
351      }
352  
353      /**
354       * Returns the {@code ContentLocalizationMap} of the {@code Content}.
355       * 
356       * @return the {@code ContentLocalizationMap} of the {@code Content}.
357       */
358      public final ContentLocalizationMap getLocalizationMap() {
359          return this.localizationMap;
360      }
361  
362      /**
363       * for JAXB only.
364       * 
365       * @return this.getLocalizationMap()
366       */
367      @XmlElement(name = "content-localization-map")
368      @Deprecated
369      protected final ContentLocalizationMap getLocalizationMapJAXB() {
370          try {
371              return getLocalizationMap();
372          } catch (LazyInitializationException e) {
373              LOGGER.debug("ignored: {}", e.getLocalizedMessage());
374              return null;
375          }
376      }
377  
378      /**
379       * Sets the {@code ContentLocalizationMap} of the {@code Content}.
380       * 
381       * @param pLocalizationMap
382       *            the {@code ContentLocalizationMap} to be set.
383       */
384      public final void setLocalizationMap(
385              final ContentLocalizationMap pLocalizationMap) {
386          if (!pLocalizationMap.containsValue(this)) {
387              pLocalizationMap.put(this.locale, this);
388          }
389          if (!this.localizationMap.equals(pLocalizationMap)) {
390              this.localizationMap.remove(this);
391          }
392          this.localizationMap = pLocalizationMap;
393      }
394  
395      /**
396       * Is called by {@code ContentLocalizationMap} to update the {@code
397       * ContentLocalizationMap} of the {@code Content}.
398       * 
399       * @param pLocalizationMap
400       *            the {@code ContentLocalizationMap} to be set.
401       */
402      protected final void updateLocalizationMap(
403              final ContentLocalizationMap pLocalizationMap) {
404          this.localizationMap = pLocalizationMap;
405      }
406  
407      /**
408       * Returns the {@code View}s of the {@code Content}.
409       * 
410       * @return the {@code View}s of the {@code Content}. (shallow copy)
411       */
412      public final Set<View> getAssociatedViews() {
413          return new HashSet<View>(this.associatedViews);
414      }
415  
416      /**
417       * For JAXB only.
418       * 
419       * @return this.getAssociatedViews()
420       */
421      @XmlElementWrapper(name = "associated-views")
422      @XmlElement(name = "view")
423      @SuppressWarnings("unused")
424      @Deprecated
425      private HashSet<ViewInfo> getAssociatedViewsJAXB() { // NOPMD
426          try {
427              return new HashSet<ViewInfo>(ViewInfo
428                      .getViewInfos(getAssociatedViews()));
429          } catch (LazyInitializationException e) {
430              LOGGER.debug("ignored: {}", e.getLocalizedMessage());
431              return null;
432          }
433      }
434  
435      /**
436       * Sets the {@code VirtualFile}s of the {@code Content}.
437       * 
438       * @param pVirtualFiles
439       *            the {@code VirtualFile}s to be set. (overwrite matching)
440       */
441      public final void setAssociatedVirtualFiles(
442              final Set<VirtualFile> pVirtualFiles) {
443          if (pVirtualFiles.getClass().equals(
444                  this.associatedVirtualFiles.getClass())) {
445              this.associatedVirtualFiles = pVirtualFiles;
446              return;
447          }
448          this.associatedVirtualFiles.clear();
449          this.associatedVirtualFiles.addAll(pVirtualFiles);
450      }
451  
452      /**
453       * Returns the {@code VirtualFile}s of the {@code Content}.
454       * 
455       * @return the {@code VirtualFile}s of the {@code Content}. (reference)
456       */
457      @XmlTransient
458      public final Set<VirtualFile> getAssociatedVirtualFiles() {
459          return this.associatedVirtualFiles;
460      }
461  
462      /**
463       * For JAXB only.
464       * 
465       * @return this.getAssociatedVirtualFiles()
466       */
467      @XmlElementWrapper(name = "associated-virtual-files")
468      @XmlElement(name = "virtual-file")
469      @SuppressWarnings("unused")
470      @Deprecated
471      private HashSet<VirtualFile> getAssociatedVirtualFilesJAXB() { // NOPMD
472          try {
473              return new HashSet<VirtualFile>(getAssociatedVirtualFiles());
474          } catch (LazyInitializationException e) {
475              LOGGER.debug("ignored: {}", e.getLocalizedMessage());
476              return null;
477          }
478      }
479  
480      /**
481       * Sets the {@code Attachment}s of the {@code Content}.
482       * 
483       * @param pAttachments
484       *            the {@code Attachment}s to be set. (overwrite matching)
485       */
486      public final void setAttachments(final Set<Attachment> pAttachments) {
487          if (pAttachments.getClass().equals(this.attachments.getClass())) {
488              this.attachments = pAttachments;
489              return;
490          }
491          this.attachments.clear();
492          this.attachments.addAll(pAttachments);
493      }
494  
495      /**
496       * Returns the {@code Attachment}s of the {@code Content}.
497       * 
498       * @return the {@code Attachment}s of the {@code Content}. (reference)
499       */
500      public final Set<Attachment> getAttachments() {
501          return this.attachments;
502      }
503  
504      /**
505       * For JAXB only.
506       * 
507       * @return this.getAttachments()
508       */
509      @XmlElementWrapper(name = "attachments")
510      @XmlElement(name = "attachment")
511      @SuppressWarnings("unused")
512      @Deprecated
513      private HashSet<Attachment> getAttachmentsJAXB() { // NOPMD
514          try {
515              return new HashSet<Attachment>(getAttachments());
516          } catch (LazyInitializationException e) {
517              LOGGER.debug("ignored: {}", e.getLocalizedMessage());
518              return null;
519          }
520      }
521  
522      /**
523       * returns whether the {@code Content} has a reference duration.
524       * 
525       * @return {@code true}, if and only if a reference duration has been set.
526       *         Otherwise {@code false}.
527       */
528      public final boolean hasReferenceDuration() {
529          return ((this.referenceDurationStart != null) && (this.referenceDurationEnd != null));
530      }
531  
532      /**
533       * returns the reference duration of the {@code Content} or {@code null}, if
534       * no {@code Duration} has been set.
535       * 
536       * @return the reference duration of the {@code Content} or {@code null}
537       */
538      @XmlElement(name = "reference-duration", required = false)
539      public final Duration getReferenceDuration() {
540          if (!hasReferenceDuration()) {
541              return null;
542          }
543          return new Duration(this.referenceDurationStart.longValue(),
544                  this.referenceDurationEnd.longValue());
545      }
546  
547      /**
548       * sets the reference duration of the {@code Content}.
549       * 
550       * @param rd
551       *            the reference duration
552       */
553      public final void setReferenceDuration(final Duration rd) {
554          this.referenceDurationStart = rd.getStartMillis();
555          this.referenceDurationEnd = rd.getEndMillis();
556      }
557  
558      /**
559       * removes the current reference {@code Duration} and returns the previous
560       * value.
561       * 
562       * @return the previous reference duration
563       */
564      public final Duration removeReferenceDuration() {
565          if (!hasReferenceDuration()) {
566              return null;
567          }
568          Duration old = new Duration(this.referenceDurationStart,
569                  this.referenceDurationEnd);
570          this.referenceDurationStart = null; // NOPMD
571          this.referenceDurationEnd = null; // NOPMD
572          return old;
573      }
574  
575      /**
576       * Determines whether the given {@code Object} is equal to the {@code
577       * Content}.
578       * 
579       * @param pObject
580       *            the {@code Object} to be checked for equality with the {@code
581       *            Content}.
582       * @return {@code true}, if the given {@code Object} is equal to the {@code
583       *         Content}. Returns {@code false}, otherwise.
584       */
585      @Override
586      public final boolean equals(final Object pObject) {
587          if ((pObject == null) || !getClass().equals(pObject.getClass())) {
588              return false;
589          }
590          Content content = (Content) pObject;
591          if ((getId() == null) && (content.getId() == null)) {
592              return super.equals(pObject);
593          } else if ((getId() == null) || (content.getId() == null)) {
594              return false;
595          }
596          return getId().equals(content.getId());
597      }
598  
599      /**
600       * Returns a hash code for the {@code Content}.
601       * 
602       * @return a hash code for the {@code Content}
603       */
604      @Override
605      public final int hashCode() {
606          if (getId() == null) {
607              return super.hashCode();
608          }
609          return getId().intValue();
610      }
611  
612      /**
613       * returns the {@code Content}'s name and id.
614       * 
615       * @return {@code getName() + " [" + getId() + "]"}
616       * @see java.lang.Object#toString()
617       */
618      @Override
619      public String toString() {
620          if (getId() == null) {
621              return super.toString();
622          }
623          return getName() + " [" + getId() + "]";
624      }
625  
626      /**
627       * Compares two {@code Content}s
628       * lexicographically.&nbsp;<strong>Note:</strong> See the details below for
629       * a full explanation.
630       * 
631       * <p>
632       * First, the names are compared lexicographically, using the {@code
633       * Content}'s {@code Locale}. If the {@code Content}'s {@code Locale} is
634       * {@code null}, the {@code Locale} of the given {@code Content} is used. If
635       * both {@code Content}s' {@code Locale}s are {@code null}, the result of
636       * {@code getName().compareTo(pContent.getName())} is used instead.
637       * </p>
638       * 
639       * <p>
640       * If the {@code Content}s' names are lexicographically equal, their {@code
641       * id}s are compared afterwards. If any of the two {@code id}s is {@code
642       * null}, the comparison scheme of {@code BasicObject} is used.
643       * </p>
644       * 
645       * @param pContent
646       *            the {@code Content} used for the comparison.
647       * @return -1, 0, or 1 as the {@code Content} is less than, equal to, or
648       *         greater than the given {@code Content}.
649       */
650      public final int compareTo(final Content pContent) {
651          Collator collator = null;
652          if (getLocale() != null) {
653              collator = Collator.getInstance(getLocale());
654          } else if (pContent.getLocale() != null) {
655              collator = Collator.getInstance(pContent.getLocale());
656          }
657          int result = 0;
658          if (collator != null) {
659              result = collator.compare(getName(), pContent.getName());
660          } else {
661              result = getName().compareTo(pContent.getName());
662          }
663          if (result != 0) {
664              return result;
665          }
666          return basicObjectCompareTo(pContent);
667      }
668  
669      /**
670       * Compares the {@code Content} to the given {@code Content}, and returns an
671       * int as the result of the comparison.
672       * 
673       * @param pContent
674       *            the {@code Content} used for the comparison.
675       * @return -1, 0, or 1 as the {@code Content} is less than, equal to, or
676       *         greater than the given {@code Content}.
677       */
678      private int basicObjectCompareTo(final Content pContent) {
679          long difference;
680          if ((getId() == null) && (pContent.getId() == null)) {
681              difference = this.hashCode() - pContent.hashCode();
682          } else if (getId() == null) {
683              return -1;
684          } else if (pContent.getId() == null) {
685              return 1;
686          } else {
687              difference = getId() - pContent.getId();
688          }
689          if (difference > 0) {
690              return 1;
691          } else if (difference < 0) {
692              return -1;
693          }
694          return 0;
695      }
696  
697      /**
698       * Returns a {@code ViewTypes} containing all {@code View}s that may be
699       * combined with the {@code Content}.
700       * 
701       * @return a {@code ViewTypes} containing all {@code View}s that may be
702       *         combined with the {@code Content}.
703       */
704      @Deprecated
705      public final ViewTypes getViewTypes() {
706          return this.bundle.getViewTypes(this);
707      }
708  
709      /**
710       * initialises the content for display.
711       */
712      public void initLazyFields() {
713          /* initialise lazy fields of Content */
714  
715          for (Attachment attachment : getAttachments()) {
716              // initialise read roles
717              if (attachment.getVirtualFile() != null) {
718                  attachment.getVirtualFile().getReadRoles().size();
719              }
720          }
721  
722          /*
723           * support for IHasMainImage and IHasAlternativeImages
724           */
725          if (this instanceof IHasMainImage) {
726              initLazyImageGroups((IHasMainImage) this);
727          }
728  
729          /* init variants */
730          if (this instanceof IHasVariants) {
731              initLazyVariants((IHasVariants) this);
732          }
733      }
734  
735      /**
736       * initialises lazy variants for contents implementing {@code IHasVariants}.
737       * 
738       * @param hasVariants
739       *            the {@code IHasVariants} to be initialised
740       */
741      public static final void initLazyVariants(final IHasVariants hasVariants) {
742          for (Variant<?> v : hasVariants.getVariants()) {
743              if (v instanceof IHasMainImage) {
744                  initLazyImageGroups((IHasMainImage) v);
745              }
746              if (v instanceof IHasVariants) {
747                  initLazyVariants((IHasVariants) v);
748              }
749          }
750      }
751  
752      /**
753       * initialises lazy image groups for contents implementing {@code
754       * IHasMainImage} and/or {@code IHasAlternativeImages}.
755       * 
756       * @param hasMainImage
757       *            the {@code IHasMainImage} to be initialised
758       */
759      public static final void initLazyImageGroups(
760              final IHasMainImage hasMainImage) {
761          AbstractImageGroup imageGroup = hasMainImage.getMainImage();
762          if (imageGroup != null) {
763              imageGroup.init();
764          }
765          if (hasMainImage instanceof IHasAlternativeImages) {
766              IHasAlternativeImages aigc = (IHasAlternativeImages) hasMainImage;
767              for (AbstractImageGroup aig : aigc.getAlternativeImages()) {
768                  if (aig != null) {
769                      aig.init();
770                  }
771              }
772          }
773      }
774  
775      /**
776       * Determines whether the {@code Content} is a group.
777       * 
778       * <p>
779       * A group is a {@code Content} generating its contents from its children in
780       * the {@code Sitemap}.
781       * </p>
782       * 
783       * @return {@code true}, if the {@code Content} is a group. Return {@code
784       *         false}, otherwise.
785       */
786      public abstract boolean isGroup();
787  
788      /**
789       * Returns the {@code StringBuilder} to be indexed for site full text
790       * searches.
791       * 
792       * @return the {@code StringBuilder} to be indexed for site full text
793       *         searches.
794       */
795      public abstract StringBuilder getFullTextValue();
796  
797      /**
798       * Is called by the {@code VirtualFileSystem}, if any associated {@code
799       * VirtualFile}s have been moved in the file system.
800       * 
801       * @param file
802       *            the file that has been moved.
803       */
804      public abstract void onVirtualFileSystemChange(final VirtualFile file);
805  
806      /**
807       * is called by the editors upon save actions to update the list of
808       * associated {@code VirtualFiles}.
809       */
810      public abstract void updateAssociatedVirtualFiles();
811  
812      /**
813       * creates and returns a non-persistent (therefore id = null) copy of the
814       * current {@code Content} with a given {@code Locale}.
815       * 
816       * @param l
817       *            the {@code Locale} to use for the the copy
818       * @param u
819       *            the {@code User} to be used as the creator/last modifier of
820       *            the copied {@code Content}
821       * 
822       * @return a copy of this {@code Content}
823       * 
824       * @throws UnsupportedOperationException
825       *             where method is not supported ({@code FilterContent}, {@code
826       *             InstructionsContent})
827       */
828      public abstract Content createCopy(Locale l, User u)
829              throws UnsupportedOperationException;
830  
831      /**
832       * Searches the given {@code Element} for {@code VirtualFile}s, adds them to
833       * the given {@code Set} of {@code VirtualFile}s and adds a {@code vfs-id}
834       * attribute to the corresponding {@code Element}.
835       * 
836       * @param html
837       *            the {@code Element} to be searched.
838       * @param files
839       *            the {@code Set} of {@code VirtualFile}s.
840       * @param vfs
841       *            an instance of the {@code VirtualFileSystem}.
842       */
843      protected static final void processHTML(final Element html,
844              final Set<VirtualFile> files, final VirtualFileSystem vfs) {
845          try {
846              VirtualFile file = null;
847              if (html.getAttribute("src") != null) {
848                  file = vfs.fromHttpURI(new URI(html.getAttributeValue("src")));
849              } else if (html.getAttribute("href") != null) {
850                  file = vfs.fromHttpURI(new URI(html.getAttributeValue("href")));
851              }
852              if (file != null) {
853                  files.add(file);
854                  html.setAttribute("vfs-id", Long.toString(file.getId()));
855              }
856          } catch (URISyntaxException e) {
857              LOGGER.warn(
858                      "Illegal URI in HTML fragment while processing HTML: {}", e
859                              .getLocalizedMessage());
860          }
861          for (Object child : html.getChildren()) {
862              processHTML((Element) child, files, vfs);
863          }
864      }
865  
866      /**
867       * Updates the {@code src} and {@code href} attributes in the given {@code
868       * Element}, referencing {@code VirtualFile}s by a {@code vfs-id} attribute.
869       * 
870       * @param html
871       *            the {@code Element} to be updated.
872       * @param file
873       *            the {@code VirtualFile}s that have been changed.
874       * @return the updated {@code Element}.
875       */
876      protected static final Element updateHTML(final Element html,
877              final VirtualFile file) {
878          if (html.getAttribute("vfs-id") != null) {
879              long vfsId = Long.parseLong(html.getAttributeValue("vfs-id"));
880              LOGGER.debug("found vfs-id: {}", vfsId);
881              if (file.getId() == vfsId) {
882                  if (html.getAttribute("href") != null) {
883                      String oldURI = html.getAttributeValue("href");
884                      StringBuilder updatedURI = buildNewVfsHttpURI(file, oldURI);
885                      LOGGER.debug("updating href '{}' to '{}'", oldURI,
886                              updatedURI);
887                      html.setAttribute("href", updatedURI.toString());
888                  }
889                  if (html.getAttribute("src") != null) {
890                      String oldURI = html.getAttributeValue("src");
891                      StringBuilder updatedURI = buildNewVfsHttpURI(file, oldURI);
892                      LOGGER.debug("updating src '{}' to '{}'", oldURI,
893                              updatedURI + "'");
894                      html.setAttribute("src", updatedURI.toString());
895                  }
896              }
897          }
898          for (Object child : html.getChildren()) {
899              updateHTML((Element) child, file);
900          }
901          return html;
902      }
903  
904      /**
905       * utility method for {@link Content#updateHTML(Element, VirtualFile)}
906       * building the updated URI string.
907       * 
908       * @param file
909       *            the virtual file
910       * @param oldURI
911       *            the old URI
912       * @return the updated URI
913       */
914      private static StringBuilder buildNewVfsHttpURI(final VirtualFile file,
915              final String oldURI) {
916          String queryString = null;
917          try {
918              queryString = new URI(oldURI).getRawQuery();
919          } catch (URISyntaxException e) {
920              LOGGER.warn("Unparsable URI: {}{}{}", new Object[] { oldURI,
921                      System.getProperty("line.separator"),
922                      e.getLocalizedMessage() });
923          }
924          StringBuilder updatedURI = new StringBuilder(file.getHttpURI()
925                  .toString());
926          if (queryString != null) {
927              updatedURI.append('?').append(queryString);
928          }
929          return updatedURI;
930      }
931  
932      /* --------------------------------------------------------------------- */
933  
934      /**
935       * Utility-Object that represents an associated {@code View} of this {@code
936       * Content} for JAXB.
937       * 
938       * @author Daniel Dietz
939       * @version $Revision: 1825 $
940       * 
941       */
942      @XmlRootElement(name = "view-info")
943      @XmlAccessorOrder(XmlAccessOrder.UNDEFINED)
944      @XmlAccessorType(XmlAccessType.FIELD)
945      private static class ViewInfo implements Serializable {
946  
947          /**
948           * The serialVersionUID.
949           */
950          private static final long serialVersionUID = -7775381150954577599L;
951  
952          /**
953           * The {@code View} of the {@code ViewInfo}.
954           */
955          @XmlTransient
956          private View view;
957  
958          /**
959           * Default constructor.
960           */
961          @SuppressWarnings("unused")
962          // required by JAXB
963          protected ViewInfo() {
964              super();
965          }
966  
967          /**
968           * Creates a new {@code ViewInfo} for the given view.
969           * 
970           * @param v
971           *            the {@code View}
972           */
973          protected ViewInfo(final View v) {
974              super();
975              setView(v);
976          }
977  
978          /**
979           * Sets the {@code View}.
980           * 
981           * @param v
982           *            the {@code View}
983           */
984          protected final void setView(final View v) {
985              if (v == null) {
986                  throw new NullPointerException(
987                          "View cannot be set nullm for ViewInfo!");
988              }
989              this.view = v;
990          }
991  
992          /**
993           * Returns the {@code View}.
994           * 
995           * @return the {@code View}
996           */
997          protected final View getView() {
998              return this.view;
999          }
1000 
1001         /**
1002          * For JAXB only.
1003          * 
1004          * @return getView().getId()
1005          */
1006         @XmlAttribute(name = "id")
1007         @SuppressWarnings("unused")
1008         @Deprecated
1009         private Long getIdJAXB() {
1010             try {
1011                 return getView().getId();
1012             } catch (LazyInitializationException e) {
1013                 LOGGER.debug("ignored: {}", e.getLocalizedMessage());
1014                 return null;
1015             }
1016         }
1017 
1018         /**
1019          * For JAXB only.
1020          * 
1021          * @return getView().getClass().getCanonicalName()
1022          */
1023         @XmlAttribute(name = "class")
1024         @SuppressWarnings("unused")
1025         @Deprecated
1026         private String getFQClassNameJAXB() {
1027             try {
1028                 return getView().getClass().getCanonicalName();
1029             } catch (LazyInitializationException e) {
1030                 LOGGER.debug("ignored: {}", e.getLocalizedMessage());
1031                 return null;
1032             }
1033         }
1034 
1035         /**
1036          * For JAXB only.
1037          * 
1038          * @return getView().getName()
1039          */
1040         @XmlElement(name = "name")
1041         @SuppressWarnings("unused")
1042         @Deprecated
1043         private String getNameJAXB() {
1044             try {
1045                 return getView().getName();
1046             } catch (LazyInitializationException e) {
1047                 LOGGER.debug("ignored: {}", e.getLocalizedMessage());
1048                 return null;
1049             }
1050         }
1051 
1052         /**
1053          * For JAXB only.
1054          * 
1055          * @return getView().getSitemapNode().getId()
1056          */
1057         @XmlElement(name = "sitemapnode-id")
1058         @SuppressWarnings("unused")
1059         @Deprecated
1060         private Long getSitemapNodeIdJAXB() {
1061             try {
1062                 return getView().getSitemapNode().getId();
1063             } catch (LazyInitializationException e) {
1064                 LOGGER.debug("ignored: {}", e.getLocalizedMessage());
1065                 return null;
1066             }
1067         }
1068 
1069         /**
1070          * For JAXB only.
1071          * 
1072          * @return getView().getContent().getId()
1073          */
1074         @XmlElement(name = "content-id")
1075         @SuppressWarnings("unused")
1076         @Deprecated
1077         private Long getContentIdJAXB() {
1078             try {
1079                 return getView().getContent().getId();
1080             } catch (LazyInitializationException e) {
1081                 LOGGER.debug("ignored: {}", e.getLocalizedMessage());
1082                 return null;
1083             }
1084         }
1085 
1086         /**
1087          * For JAXB only.
1088          * 
1089          * @return getView().getCommandBuilder()
1090          */
1091         @XmlElement(name = "command-builder")
1092         @SuppressWarnings("unused")
1093         @Deprecated
1094         private CommandBuilder getCommandBuilderIdJAXB() {
1095             try {
1096                 return getView().getCommandBuilder();
1097             } catch (LazyInitializationException e) {
1098                 LOGGER.debug("ignored: {}", e.getLocalizedMessage());
1099                 return null;
1100             }
1101         }
1102 
1103         /**
1104          * Utility method to build a {@code Set&lt;ViewInfo&gt;} from a given
1105          * {@code Collection&lt;View&gt;}.
1106          * 
1107          * @param views
1108          *            the {@code Collection&lt;View&gt;}
1109          * 
1110          * @return a {@code Set&lt;ViewInfo&gt;}
1111          */
1112         protected static final Set<ViewInfo> getViewInfos(
1113                 final Collection<View> views) {
1114             Set<ViewInfo> viewInfos = new HashSet<ViewInfo>();
1115             for (View c : views) {
1116                 viewInfos.add(new ViewInfo(c));
1117             }
1118             return viewInfos;
1119         }
1120 
1121     }
1122 
1123 }
1124