1    /*
2     * Copyright 2008 :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.util.Collection;
22   import java.util.Comparator;
23   import java.util.HashSet;
24   import java.util.Locale;
25   import java.util.Map;
26   import java.util.Map.Entry;
27   import java.util.Set;
28   import java.util.TreeMap;
29   
30   import javax.persistence.CascadeType;
31   import javax.persistence.Entity;
32   import javax.persistence.FetchType;
33   import javax.persistence.MapKeyColumn;
34   import javax.persistence.OneToMany;
35   import javax.xml.bind.annotation.XmlAccessOrder;
36   import javax.xml.bind.annotation.XmlAccessType;
37   import javax.xml.bind.annotation.XmlAccessorOrder;
38   import javax.xml.bind.annotation.XmlAccessorType;
39   import javax.xml.bind.annotation.XmlAttribute;
40   import javax.xml.bind.annotation.XmlElement;
41   import javax.xml.bind.annotation.XmlElementWrapper;
42   import javax.xml.bind.annotation.XmlRootElement;
43   import javax.xml.bind.annotation.XmlTransient;
44   import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
45   
46   import org.hibernate.LazyInitializationException;
47   import org.jdom.Element;
48   import org.slf4j.Logger;
49   import org.slf4j.LoggerFactory;
50   import org.torweg.pulse.bundle.Bundle;
51   import org.torweg.pulse.bundle.JDOMable;
52   import org.torweg.pulse.service.PulseException;
53   import org.torweg.pulse.util.entity.AbstractBasicEntity;
54   import org.torweg.pulse.util.xml.bind.LocaleXmlAdapter;
55   
56   /**
57    * marks a set of {@code Content}s to be localizations of each other.
58    * <p>
59    * Within a {@code ContentLocalizationMap} each {@code Locale} and {@code
60    * Content} may only appear once. Moreover all {@code Content}s have to be of
61    * the exact same type.
62    * </p>
63    * <p>
64    * <em>The {@link ContentLocalizationMap#put(Locale, Content)} method does not comply with the general
65    * {@code Map} contract, as it does not support the overwriting of
66    * values!</em>
67    * </p>
68    * 
69    * @author Thomas Weber, Daniel Dietz
70    * @version $Revision: 1822 $
71    */
72   @Entity
73   @XmlRootElement(name = "content-localization-map")
74   @XmlAccessorOrder(XmlAccessOrder.UNDEFINED)
75   @XmlAccessorType(XmlAccessType.FIELD)
76   public class ContentLocalizationMap extends AbstractBasicEntity implements
77           JDOMable {
78   
79       /**
80        * serialVersionUID.
81        */
82       private static final long serialVersionUID = 8867155586696229373L;
83   
84       /**
85        * the logger.
86        */
87       private static final Logger LOGGER = LoggerFactory
88               .getLogger(ContentLocalizationMap.class);
89   
90       /**
91        * the internal map of locales and contents.
92        */
93       @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
94       @MapKeyColumn(name = "locale")
95       @XmlTransient
96       // -> this.getMap()
97       private final Map<Locale, Content> map = new TreeMap<Locale, Content>(
98               new LocaleStringComparator());
99   
100      /**
101       * used by Hibernate<sup>TM</sup>.
102       */
103      @Deprecated
104      protected ContentLocalizationMap() {
105          super();
106      }
107  
108      /**
109       * creates a new {@code ContentLocalizationMap} for the given {@code
110       * Content} and the given {@code Locale}.
111       * 
112       * @param l
113       *            the locale
114       * @param c
115       *            the content
116       */
117      public ContentLocalizationMap(final Locale l, final Content c) {
118          super();
119          put(l, c);
120      }
121  
122      /**
123       * removes all mappings from this map.
124       */
125      public final void clear() {
126          for (Locale l : this.map.keySet()) {
127              remove(l);
128          }
129      }
130  
131      /**
132       * returns <tt>true</tt> if this map contains a mapping for the specified
133       * key. More formally, returns <tt>true</tt> if and only if this map
134       * contains a mapping for a key <tt>k</tt> such that
135       * <tt>(key==null ? k==null : key.equals(k))</tt>. (There can be at most one
136       * such mapping.)
137       * 
138       * @param key
139       *            key whose presence in this map is to be tested.
140       * @return <tt>true</tt> if this map contains a mapping for the specified
141       *         key.
142       * 
143       * @throws ClassCastException
144       *             if the key is of an inappropriate type for this map.
145       * @throws NullPointerException
146       *             if the key is <tt>null</tt>.
147       */
148      // CHECKSTYLE:OFF (unchecked Exception)
149      public final boolean containsKey(final Object key)
150              throws ClassCastException, NullPointerException {
151          // CHECKSTYLE:ON
152          return this.map.containsKey((Locale) key);
153      }
154  
155      /**
156       * returns <tt>true</tt> if this map maps one or more keys to the specified
157       * value. More formally, returns <tt>true</tt> if and only if this map
158       * contains at least one mapping to a value <tt>v</tt> such that
159       * <tt>(value==null ? v==null : value.equals(v))</tt>. This operation will
160       * probably require time linear in the map size for most implementations of
161       * the <tt>Map</tt> interface.
162       * 
163       * @param value
164       *            value whose presence in this map is to be tested.
165       * @return <tt>true</tt> if this map maps one or more keys to the specified
166       *         value.
167       * @throws ClassCastException
168       *             if the value is of an inappropriate type for this map.
169       * @throws NullPointerException
170       *             if the value is <tt>null</tt> and this map does not permit
171       *             <tt>null</tt> values.
172       */
173      // CHECKSTYLE:OFF (unchecked Exception)
174      public final boolean containsValue(final Object value)
175              throws ClassCastException, NullPointerException {
176          // CHECKSTYLE:ON
177          return this.map.containsValue((Content) value);
178      }
179  
180      /**
181       * returns a set view of the mappings contained in this map. Each element in
182       * the returned set is a {@link Entry}. The set is backed by the map, so
183       * changes to the map are reflected in the set, and vice-versa. If the map
184       * is modified while an iteration over the set is in progress (except
185       * through the iterator's own <tt>remove</tt> operation, or through the
186       * <tt>setValue</tt> operation on a map entry returned by the iterator) the
187       * results of the iteration are undefined. The set supports element removal,
188       * which removes the corresponding mapping from the map, via the
189       * <tt>Iterator.remove</tt>, <tt>Set.remove</tt>, <tt>removeAll</tt>,
190       * <tt>retainAll</tt> and <tt>clear</tt> operations. It does not support the
191       * <tt>add</tt> or <tt>addAll</tt> operations.
192       * 
193       * @return a set view of the mappings contained in this map.
194       */
195      public final Set<java.util.Map.Entry<Locale, Content>> entrySet() {
196          return this.map.entrySet();
197      }
198  
199      /**
200       * returns the value to which this map maps the specified key. Returns
201       * <tt>null</tt> if the map contains no mapping for this key. A return value
202       * of <tt>null</tt> does not <i>necessarily</i> indicate that the map
203       * contains no mapping for the key; it's also possible that the map
204       * explicitly maps the key to <tt>null</tt>. The <tt>containsKey</tt>
205       * operation may be used to distinguish these two cases.
206       * 
207       * <p>
208       * More formally, if this map contains a mapping from a key <tt>k</tt> to a
209       * value <tt>v</tt> such that <tt>(key==null ? k==null :
210       * key.equals(k))</tt>, then this method returns <tt>v</tt>; otherwise it
211       * returns <tt>null</tt>. (There can be at most one such mapping.)
212       * 
213       * @param key
214       *            key whose associated value is to be returned.
215       * @return the value to which this map maps the specified key, or
216       *         <tt>null</tt> if the map contains no mapping for this key.
217       * 
218       * @throws ClassCastException
219       *             if the key is of an inappropriate type for this map.
220       * @throws NullPointerException
221       *             if the key is <tt>null</tt>.
222       * 
223       * @see #containsKey(Object)
224       */
225      // CHECKSTYLE:OFF (unchecked Exception)
226      public final Content get(final Object key) throws ClassCastException,
227              NullPointerException {
228          // CHECKSTYLE:ON
229          return this.map.get((Locale) key);
230      }
231  
232      /**
233       * returns <tt>true</tt> if this map contains no key-value mappings.
234       * 
235       * @return <tt>true</tt> if this map contains no key-value mappings.
236       */
237      public final boolean isEmpty() {
238          return this.map.isEmpty();
239      }
240  
241      /**
242       * returns a set view of the keys contained in this map. The set is backed
243       * by the map, so changes to the map are reflected in the set, and
244       * vice-versa. If the map is modified while an iteration over the set is in
245       * progress (except through the iterator's own <tt>remove</tt> operation),
246       * the results of the iteration are undefined. The set supports element
247       * removal, which removes the corresponding mapping from the map, via the
248       * <tt>Iterator.remove</tt>, <tt>Set.remove</tt>, <tt>removeAll</tt>
249       * <tt>retainAll</tt>, and <tt>clear</tt> operations. It does not support
250       * the add or <tt>addAll</tt> operations.
251       * 
252       * @return a set view of the keys contained in this map.
253       */
254      public final Set<Locale> keySet() {
255          return this.map.keySet();
256      }
257  
258      /**
259       * puts a new mapping from {@code key} to {@code value} provided
260       * <em>both</em> the {@code key} and {@code value} are not yet part of the
261       * {@code ContentLocalizationMap} and the {@code Content} is of the same
262       * type as the other values already present in the map.
263       * 
264       * @param key
265       *            the locale
266       * @param value
267       *            the content
268       * @return {@code null} (to match the {@code Map} interface)
269       * @throws InconsistentLocalizationException
270       *             when a precondition for putting the association fails
271       */
272      // CHECKSTYLE:OFF (unchecked Exception)
273      public final Content put(final Locale key, final Content value)
274              throws InconsistentLocalizationException {
275          // CHECKSTYLE:ON
276          if (this.map.containsKey(key)) {
277              throw new InconsistentLocalizationException("The locale " + key
278                      + " is already present.");
279          } else if (this.map.containsValue(value)) {
280              throw new InconsistentLocalizationException("The content "
281                      + value.getName() + " [" + value.getId()
282                      + "] is already present.");
283          } else if (!key.equals(value.getLocale())) {
284              throw new InconsistentLocalizationException(
285                      "The given content and locale do not match.");
286          } else if (!this.map.isEmpty()
287                  && !value.getClass().equals(
288                          this.map.values().iterator().next().getClass())) {
289              throw new InconsistentLocalizationException("The given Content '"
290                      + value.getClass().getCanonicalName()
291                      + "' does not match the Content type of the Map '"
292                      + this.map.values().iterator().next().getClass()
293                              .getCanonicalName() + "'.");
294          } else if (value.getLocalizationMap() != null) {
295              value.getLocalizationMap().updateRemoveContent(value);
296          }
297          value.updateLocalizationMap(this);
298          return this.map.put(key, value);
299      }
300  
301      /**
302       * internal method... TODO
303       * 
304       * @param value
305       *            x
306       */
307      private void updateRemoveContent(final Content value) {
308          for (Map.Entry<Locale, Content> entry : this.map.entrySet()) {
309              if (entry.getValue().equals(value)) {
310                  this.map.remove(entry.getKey());
311                  break;
312              }
313          }
314      }
315  
316      /**
317       * copies all of the mappings from the specified map to this map. The effect
318       * of this call is equivalent to that of calling
319       * {@link ContentLocalizationMap#put(Locale, Content)} on this map once for
320       * each mapping from key <tt>k</tt> to value <tt>v</tt> in the specified
321       * map. The behaviour of this operation is unspecified if the specified map
322       * is modified while the operation is in progress.
323       * 
324       * @param t
325       *            Mappings to be stored in this map.
326       */
327      public final void putAll(final Map<? extends Locale, ? extends Content> t) {
328          for (Map.Entry<? extends Locale, ? extends Content> entry : t
329                  .entrySet()) {
330              put(entry.getKey(), entry.getValue());
331          }
332  
333      }
334  
335      /**
336       * removes the mapping for this key from this map if it is present. More
337       * formally, if this map contains a mapping from key <tt>k</tt> to value
338       * <tt>v</tt> such that {@code (key==null ? k==null : key.equals(k))}, that
339       * mapping is removed. (The map can contain at most one such mapping.)
340       * 
341       * <p>
342       * Returns the value to which the map previously associated the key, or
343       * <tt>null</tt> if the map contained no mapping for this key. (A
344       * <tt>null</tt> return can also indicate that the map previously associated
345       * <tt>null</tt> with the specified key if the implementation supports
346       * <tt>null</tt> values.) The map will not contain a mapping for the
347       * specified key once the call returns.
348       * 
349       * @param key
350       *            key whose mapping is to be removed from the map.
351       * @return previous value associated with specified key, or <tt>null</tt> if
352       *         there was no mapping for key.
353       * 
354       * @throws ClassCastException
355       *             if the key is of an inappropriate type for this map.
356       * @throws NullPointerException
357       *             if the key is <tt>null</tt>.
358       */
359      // CHECKSTYLE:OFF (unchecked Exception)
360      public final Content remove(final Object key) throws ClassCastException,
361              NullPointerException {
362          // CHECKSTYLE:ON
363          Content c = this.map.remove((Locale) key);
364          if (c != null) {
365              c
366                      .updateLocalizationMap(new ContentLocalizationMap(
367                              (Locale) key, c));
368          }
369          return c;
370      }
371  
372      /**
373       * returns the number of key-value mappings in this map. If the map contains
374       * more than <tt>Integer.MAX_VALUE</tt> elements, returns
375       * <tt>Integer.MAX_VALUE</tt>.
376       * 
377       * @return the number of key-value mappings in this map.
378       */
379      public final int size() {
380          return this.map.size();
381      }
382  
383      /**
384       * returns a collection view of the values contained in this map. The
385       * collection is backed by the map, so changes to the map are reflected in
386       * the collection, and vice-versa. If the map is modified while an iteration
387       * over the collection is in progress (except through the iterator's own
388       * <tt>remove</tt> operation), the results of the iteration are undefined.
389       * The collection supports element removal, which removes the corresponding
390       * mapping from the map, via the <tt>Iterator.remove</tt>,
391       * <tt>Collection.remove</tt>, <tt>removeAll</tt>, <tt>retainAll</tt> and
392       * <tt>clear</tt> operations. It does not support the add or <tt>addAll</tt>
393       * operations.
394       * 
395       * @return a collection view of the values contained in this map.
396       */
397      public final Collection<Content> values() {
398          return this.map.values();
399      }
400  
401      /**
402       * For JAXB only.
403       * 
404       * @return this.values()
405       */
406      @XmlElementWrapper(name = "contents")
407      @XmlElement(name = "content")
408      @SuppressWarnings("unused")
409      @Deprecated
410      private HashSet<ContentInfo> getMap() { // NOPMD
411          try {
412              return new HashSet<ContentInfo>(ContentInfo
413                      .getContentInfos(values()));
414          } catch (LazyInitializationException e) {
415              LOGGER.debug("ignored: {}", e.getLocalizedMessage());
416              return null;
417          }
418      }
419  
420      /**
421       * Always throws {@code PulseException}.
422       * <p>
423       * <strong>ONLY here to satisfy:</strong> {@code ContentLocalizationMap}
424       * extends {@code AbstractBasicEntity}.
425       * </p>
426       * 
427       * 
428       * @return always throws {@code PulseException}
429       */
430      public final Element deserializeToJDOM() {
431          throw new PulseException("Method not supported!");
432      }
433  
434      /* --------------------------------------------------------------------- */
435  
436      /**
437       * Utility-Object that represents a content of this {@code
438       * ContentLoaclizationMap} for JAXB.
439       * 
440       * @author Daniel Dietz
441       * @version $Revision: 1822 $
442       * 
443       */
444      @XmlRootElement(name = "content-info")
445      @XmlAccessorOrder(XmlAccessOrder.UNDEFINED)
446      @XmlAccessorType(XmlAccessType.FIELD)
447      private static class ContentInfo implements Serializable {
448  
449          /**
450           * The serialVersionUID.
451           */
452          private static final long serialVersionUID = -7775381150954577599L;
453  
454          /**
455           * The {@code Content} of the {@code ContentInfo}.
456           */
457          @XmlTransient
458          private Content content;
459  
460          /**
461           * Default constructor for JAXB.
462           */
463          @Deprecated
464          @SuppressWarnings("unused")
465          protected ContentInfo() {
466              super();
467          }
468  
469          /**
470           * Creates a new {@code ContentInfo} for the given content.
471           * 
472           * @param c
473           *            the {@code Content}
474           */
475          protected ContentInfo(final Content c) {
476              super();
477              setContent(c);
478          }
479  
480          /**
481           * Sets the {@code Content}.
482           * 
483           * @param c
484           *            the {@code Content}
485           */
486          protected final void setContent(final Content c) {
487              this.content = c;
488          }
489  
490          /**
491           * Returns the {@code Content}.
492           * 
493           * @return the {@code Content}
494           */
495          protected final Content getContent() {
496              return this.content;
497          }
498  
499          /**
500           * For JAXB only.
501           * 
502           * @return getContent().getId()
503           */
504          @XmlAttribute(name = "id")
505          @SuppressWarnings("unused")
506          @Deprecated
507          private Long getIdJAXB() {
508              try {
509                  return getContent().getId();
510              } catch (LazyInitializationException e) {
511                  LOGGER.debug("ignored: {}", e.getLocalizedMessage());
512                  return null;
513              }
514          }
515  
516          /**
517           * For JAXB only.
518           * 
519           * @return getContent().getClass().getCanonicalName()
520           */
521          @XmlAttribute(name = "class")
522          @SuppressWarnings("unused")
523          @Deprecated
524          private String getFQClassNameJAXB() {
525              try {
526                  return getContent().getClass().getCanonicalName();
527              } catch (LazyInitializationException e) {
528                  LOGGER.debug("ignored: {}", e.getLocalizedMessage());
529                  return null;
530              }
531          }
532  
533          /**
534           * For JAXB only.
535           * 
536           * @return getContent().getName()
537           */
538          @XmlElement(name = "name")
539          @SuppressWarnings("unused")
540          @Deprecated
541          private String getNameJAXB() {
542              try {
543                  return getContent().getName();
544              } catch (LazyInitializationException e) {
545                  LOGGER.debug("ignored: {}", e.getLocalizedMessage());
546                  return null;
547              }
548          }
549  
550          /**
551           * For JAXB only.
552           * 
553           * @return getContent().getBundle()
554           */
555          @XmlElement(name = "bundle")
556          @SuppressWarnings("unused")
557          @Deprecated
558          private Bundle getBundleJAXB() {
559              try {
560                  return getContent().getBundle();
561              } catch (LazyInitializationException e) {
562                  LOGGER.debug("ignored: {}", e.getLocalizedMessage());
563                  return null;
564              }
565          }
566  
567          /**
568           * For JAXB only.
569           * 
570           * @return getContent().getLocale()
571           */
572          @XmlElement(name = "locale")
573          @XmlJavaTypeAdapter(value = LocaleXmlAdapter.class)
574          @SuppressWarnings("unused")
575          @Deprecated
576          private Locale getLocaleJAXB() {
577              try {
578                  return getContent().getLocale();
579              } catch (LazyInitializationException e) {
580                  LOGGER.debug("ignored: {}", e.getLocalizedMessage());
581                  return null;
582              }
583          }
584  
585          /**
586           * Utility method to build a {@code Set&lt;ContentInfo&gt;} from a given
587           * {@code Collection&lt;Content&gt;}.
588           * 
589           * @param contents
590           *            the {@code Collection&lt;Content&gt;}
591           * 
592           * @return a {@code Set&lt;ContentInfo&gt;}
593           */
594          protected static final Set<ContentInfo> getContentInfos(
595                  final Collection<Content> contents) {
596              Set<ContentInfo> contentInfos = new HashSet<ContentInfo>();
597              for (Content c : contents) {
598                  contentInfos.add(new ContentInfo(c));
599              }
600              return contentInfos;
601          }
602  
603      }
604  
605      /* --------------------------------------------------------------------- */
606  
607      /**
608       * compares two locales string-wise.
609       */
610      private static class LocaleStringComparator implements Comparator<Locale>,
611              Serializable {
612  
613          /**
614           * serialVersionUID.
615           */
616          private static final long serialVersionUID = -8798128443586879106L;
617  
618          /**
619           * compares the two locales by their string values.
620           * 
621           * @param o1
622           *            the first locale
623           * @param o2
624           *            the second locale
625           * @see Comparator#compare(Object, Object)
626           * @return the result of the comparison
627           */
628          public final int compare(final Locale o1, final Locale o2) {
629              return o1.toString().compareTo(o2.toString());
630          }
631  
632      }
633  }
634