View Javadoc

1   /*
2    * Copyright (C) 2004-2006 Christian Siefkes <christian@siefkes.net>.
3    * Development of this software is supported by the German Research Society,
4    * Berlin-Brandenburg Graduate School in Distributed Information Systems
5    * (DFG grant no. GRK 316).
6    *
7    * This program is free software; you can redistribute it and/or modify
8    * it under the terms of the GNU General Public License as published by
9    * the Free Software Foundation; either version 2 of the License, or
10   * (at your option) any later version.
11   *
12   * This program is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   * GNU General Public License for more details.
16   *
17   * You should have received a copy of the GNU General Public License
18   * along with this program; if not, visit
19   * http://www.gnu.org/licenses/gpl.html or write to the Free Software
20   * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
21   */
22  package de.fu_berlin.ties.io;
23  
24  import java.io.File;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.OutputStream;
28  import java.io.Reader;
29  import java.io.Writer;
30  import java.util.Iterator;
31  import java.util.LinkedHashSet;
32  import java.util.LinkedList;
33  import java.util.List;
34  
35  import org.apache.commons.configuration.Configuration;
36  import org.apache.commons.lang.builder.ToStringBuilder;
37  import org.dom4j.Element;
38  import org.dom4j.QName;
39  import org.dom4j.tree.DefaultElement;
40  
41  import de.fu_berlin.ties.TiesConfiguration;
42  import de.fu_berlin.ties.xml.dom.DOMUtils;
43  
44  /***
45   * A container of {@link de.fu_berlin.ties.io.FieldMap}s. A container stores all
46   * field maps added to itself and keeps a set of all keys found in the field
47   * maps. This is an "append-only" container; field maps can only be added but
48   * never removed.
49   *
50   * <p>Instances of this class are not thread-safe and must be synchronized
51   * externally, if required.
52   *
53   * @author Christian Siefkes
54   * @version $Revision: 1.26 $, $Date: 2006/10/21 16:04:22 $, $Author: siefkes $
55   */
56  public class FieldContainer implements XMLStorable {
57  
58      /***
59       * Factory method that creates a field container in {@link DelimSepValues}
60       * format.
61       *
62       * @param config used to configure the container
63       * @return the created container
64       */
65      public static FieldContainer createFieldContainer(
66              final TiesConfiguration config) {
67          return new DelimSepValues(config);
68      }
69  
70      /***
71       * Returns the file extension recommended for {@link FieldContainer}s.
72       *
73       * @return the recommended extension: {@link DelimSepValues#FILE_EXT}
74       */
75      public static String recommendedExtension() {
76          return DelimSepValues.FILE_EXT;
77      }
78  
79      /***
80       * Convenience method that serializes the contents of a container in a
81       * file in standard format, delegating to
82       * {@link #createFieldContainer(TiesConfiguration)} and
83       * {@link #storeInFile(File, String, String, Configuration)}.
84       *
85       * @param sContainer the container to serialize
86       * @param directory the directory in which to store the data
87       * @param baseName the base name of the file using for storing the data
88       * @param extension the file extension to use; if <code>null</code>,
89       * the {@linkplain #recommendedExtension() recommended extension} is used
90       * @param config used to determine the container format and output character
91       * set
92       * @return the file used for storing the data
93       * @throws IOException if an I/O error occurs
94       */
95      public static File storeContainerInFile(final StorableContainer sContainer,
96              final File directory, final String baseName,
97              final String extension, final TiesConfiguration config)
98      throws IOException {
99          final FieldContainer fContainer = createFieldContainer(config);
100         sContainer.storeEntries(fContainer);
101         return fContainer.storeInFile(directory, baseName, extension, config);
102     }
103 
104     /***
105      * This map can be used to extend newly added field maps: at each
106      * {@link #add(FieldMap)} operation, any key/value pairs from this map are
107      * added to field map prior to storing it. The map is empty by default.
108      */
109     private final FieldMap backgroundMap = new FieldMap();
110 
111     /***
112      * A list of all the field maps stored in this container.
113      */
114     private final LinkedList<FieldMap> entries = new LinkedList<FieldMap>();
115 
116     /***
117      * A set containing all keys from the stored field maps.
118      */
119     private final LinkedHashSet<String> keySet = new LinkedHashSet<String>();
120 
121 
122     /***
123      * Creates a new empty instance.
124      */
125     public FieldContainer() {
126         super();
127     }
128 
129     /***
130      * Creates a new instance from an XML element, fulfilling the
131      * recommandation of the {@link XMLStorable} interface.
132      *
133      * @param element the XML element containing the serialized representation
134      */
135     public FieldContainer(final Element element) {
136         this();
137         final Iterator outerIter = element.elementIterator();
138         Iterator innerIter;
139         Element childElement;
140         int sectionNo = -1;
141 
142         // iterate child elements of main element
143         while (outerIter.hasNext()) {
144             childElement = (Element) outerIter.next();
145             innerIter = childElement.elementIterator();
146 
147             if (innerIter.hasNext()) {
148                 // element with childrens are as level 2 element: start new
149                 // section + process children
150                 sectionNo++;
151 
152                 while (innerIter.hasNext()) {
153                     // convert each child element into a field map
154                     add(new FieldMap((Element) innerIter.next(), sectionNo));
155                 }
156             } else {
157                 // avoid negative section numbers
158                 if (sectionNo < 0) {
159                     sectionNo = 0;
160                 }
161 
162                 // convert child element into a field map
163                 add(new FieldMap(childElement, sectionNo));
164             }
165         }
166     }
167 
168 
169     /***
170      * Helper method that adds a key to the set of all keys.
171      *
172      * @param key the key to add
173      * @return <code>true</code> if the set did not already contain the
174      * specified element
175      */
176     protected boolean addKey(final String key) {
177         return keySet.add(key);
178     }
179 
180     /***
181      * Adds a field map to this container. Any new keys in this set are added
182      * at the end of the set of all keys, in the order of the field map's key
183      * set.
184      *
185      * @param map the field map to add
186      */
187     public void add(final FieldMap map) {
188         // we want to check keys
189         add(map, true);
190     }
191 
192     /***
193      * Adds the representation of a {@link Storable} to this container, by
194      * calling its {@link Storable#storeFields()} method and adding the
195      * resulting field map.
196      *
197      * @param storable the storable whose fields to add
198      */
199     public void add(final Storable storable) {
200         add(storable.storeFields());
201     }
202 
203     /***
204      * Adds the contents of a {@link StorableContainer} to this container, by
205      * delegating to {@link StorableContainer#storeEntries(FieldContainer)}.
206      *
207      * @param contents the contents to add by calling
208      * {@link StorableContainer#storeEntries(FieldContainer)}
209      */
210     public void add(final StorableContainer contents) {
211         contents.storeEntries(this);
212     }
213 
214     /***
215      * Helper method for adding a field map to this container.
216      *
217      * @param map the field map to add
218      * @param checkKeys whether to check the keys of the field map and add any
219      * missing keys to the set of all keys; subclasses calling this method can
220      * set this to <code>false</code> iff they know for sure that the map does
221      * not contain any new keys
222      */
223     protected void add(final FieldMap map, final boolean checkKeys) {
224         // add entries from the background map, if any
225         if (!backgroundMap.isEmpty()) {
226             map.putAll(backgroundMap);
227         }
228 
229         entries.add(map);
230 
231         if (checkKeys) {
232             // append any new keys at the end of the key set, in iteration order
233             keySet.addAll(map.keySet());
234         }
235     }
236 
237     /***
238      * Adds a field map created from the specified values, using the
239      * <em>n</em>-th key from the set of all keys for the <em>n</em>-th
240      * specified value. Empty strings and <code>null</code> values are omitted
241      * (the corresponding key is skipped).
242      *
243      * @param values the list of values to store in the field map
244      * @param sectionNo the number of the current
245      * {@linkplain FieldMap#getSection() section}
246      * @throws IllegalArgumentException if the specified array contains more
247      * elements than the set of all keys
248      */
249     public void add(final List values, final int sectionNo)
250     throws IllegalArgumentException {
251         if (values.size() > keySet.size()) {
252             throw new IllegalArgumentException(
253                 "Cannot create FieldMap: more values (" + values.size()
254                 + ") than keys (" + keySet.size() + ")");
255         }
256 
257         // create and populate map
258         final FieldMap fieldMap = new FieldMap(sectionNo);
259         final Iterator<String> keyIter = keyIterator();
260         final Iterator valueIter = values.iterator();
261         String currentKey;
262         Object currentValue;
263 
264         while (valueIter.hasNext()) {
265             // we checked above that the key iterator has enough elements
266             currentKey = keyIter.next();
267             currentValue = valueIter.next();
268 
269             // skip null and empty strings
270             if ((currentValue != null) && (!"".equals(currentValue))) {
271                 fieldMap.put(currentKey, currentValue);
272             }
273         }
274 
275         // no need to check the keys, as they are taken from the set
276         add(fieldMap, false);
277     }
278 
279     /***
280      * This map can be used to extend newly added field maps: at each
281      * {@link #add(FieldMap)} operation, any key/value pairs from this map are
282      * added to field map prior to storing it. The map is empty by default.
283      *
284      * @return the background map
285      */
286     public FieldMap backgroundMap() {
287         return backgroundMap;
288     }
289 
290     /***
291      * Creates (deserializes) an list of objects of a specified type by calling
292      * {@link FieldMap#createObject(Class)} for each of the field maps contained
293      * in this container. The contained field maps are deserialized in the order
294      * they were added to this container.
295      *
296      * @param type the class of the objects to create; must have a constructor
297      * whose only argument is a <code>FieldMap</code>
298      * @return a list of the created object; all elements of this list will be
299      * instances of the specified class
300      * @throws InstantiationException if instantiation failed
301      * @throws SecurityException if access to the required reflection
302      * information is denied
303      */
304     public List<Object> createObjects(final Class type)
305     throws InstantiationException, SecurityException {
306         final List<Object> result = new LinkedList<Object>();
307         final Iterator entryIter = entryIterator();
308         FieldMap currentMap;
309 
310         // create and store an object from each field map
311         while (entryIter.hasNext()) {
312             currentMap = (FieldMap) entryIter.next();
313             result.add(currentMap.createObject(type));
314         }
315         return result;
316     }
317 
318     /***
319      * Returns an iterator over the {@link FieldMap}s in this container in the
320      * order they were added.
321      *
322      * @return an iterator over the contained field maps
323      */
324     public Iterator<FieldMap> entryIterator() {
325         return entries.iterator();
326     }
327 
328     /***
329      * Returns the number of keys in this container.
330      *
331      * @return the number of keys, i.e. the size of the set of all keys
332      */
333     public int keyCount() {
334         return keySet.size();
335     }
336 
337     /***
338      * Returns an iterator over the set of all keys used in contained field
339      * maps.
340      *
341      * @return an iterator over the set of all keys
342      */
343     public Iterator<String> keyIterator() {
344         return keySet.iterator();
345     }
346 
347     /***
348      * Subclasses can overwrite this method to deserialize their contents
349      * in a class-specific format. This class does not prescribe a specific
350      * format and thus cannot read the data, throwing an
351      * {@link UnsupportedOperationException} instead.
352      *
353      * @param input the input data to process
354      */
355     public void read(final CharSequence input) {
356         throw new UnsupportedOperationException(
357                 "Reading is not supported by FieldContainer");
358     }
359 
360     /***
361      * Read deserialized data. This method wraps the stream in a
362      * {@linkplain IOUtils#openUnicodeReader(InputStream) Unicode reader} and
363      * delegates to {@link #read(Reader)}. Internally uses
364      * {@link IOUtils#openCompressableInStream(InputStream)} so
365      * <code>gzip</code>-compressed streams are also supported.
366      *
367      * @param in the input stream to read from; not closed by this method
368      * @throws IOException if an I/O error occurs while reading the data
369      */
370     public final void read(final InputStream in) throws IOException {
371         read(IOUtils.openUnicodeReader(IOUtils.openCompressableInStream(in)));
372     }
373 
374     /***
375      * Read deserialized data. This method delegates to
376      * {@link #read(CharSequence)}.
377      *
378      * @param reader the reader to read from; not closed by this method
379      * @throws IOException if an I/O error occurs while reading the data
380      */
381     public final void read(final Reader reader) throws IOException {
382         read(IOUtils.readToString(reader));
383     }
384 
385     /***
386      * Returns the number of entries stored in this container.
387      *
388      * @return the number of entries
389      */
390     public int size() {
391         return entries.size();
392     }
393 
394     /***
395      * Serializes contents by wrapping the stream in a
396      * {@linkplain IOUtils#openUnicodeWriter(OutputStream) Unicode writer} and
397      * delegating to {@link #store(Writer)}.
398      *
399      * @param out the output stream to write to; flushed but not closed by this
400      * method
401      * @throws IOException if an I/O error occurs while writing to the stream
402      */
403     public final void store(final OutputStream out) throws IOException {
404         // wrap in writer with UTF-8 charset
405         store(IOUtils.openUnicodeWriter(out));
406     }
407 
408     /***
409      * Subclasses can overwrite this method to serialize their contents
410      * in a class-specific format. This class does not prescribe a specific
411      * format and thus cannot store the data, throwing an
412      * {@link UnsupportedOperationException} instead.
413      *
414      * @param writer the writer to write to; not closed by this method
415      * @throws IOException if an I/O error occurs while serializing the data
416      */
417     public void store(final Writer writer) throws IOException {
418         throw new UnsupportedOperationException(
419                 "Storing is not supported by FieldContainer");
420     }
421 
422     /***
423      * Convenience method for serializing the contents of this container in a
424      * file.
425      *
426      * @param directory the directory in which to store the data
427      * @param baseName the base name of the file using for storing the data
428      * @param extension the file extension to use; if <code>null</code>,
429      * the {@linkplain #recommendedExtension() recommended extension} is used
430      * @param config used to determine the output character set (by delegating
431      * to {@link IOUtils#openWriter(File, Configuration)}
432      * @return the file used for storing the data
433      * @throws IOException if an I/O error occurs
434      */
435     public File storeInFile(final File directory, final String baseName,
436             final String extension, final Configuration config)
437     throws IOException {
438 
439         // use default extension if none is specified
440         final String usedExtension = ((extension != null)
441                 ? extension : FieldContainer.recommendedExtension());
442         final File outFile = IOUtils.createOutFile(directory, baseName,
443             usedExtension);
444         final Writer writer = IOUtils.openWriter(outFile, config);
445         store(writer);
446         writer.flush();
447         IOUtils.tryToClose(writer);
448         return outFile;
449     }
450 
451     /***
452      * {@inheritDoc} This implementation delegates to {@link
453      * #toElement(String)}, setting the name of the main element to "list".
454      */
455     public ObjectElement toElement() {
456         return toElement("list");
457     }
458 
459     /***
460      * Stores all contents of this container in an XML element for
461      * serialization, without using a medium level.
462      *
463      * @param name the qualified name to use for the main element
464      * @return the created XML element
465      */
466     public ObjectElement toElement(final QName name) {
467         return toElement(name, null);
468     }
469 
470     /***
471      * Stores all contents of this container in an XML element for
472      * serialization. If a <code>level2Name</code> is given, an element of this
473      * name is created for each non-empty {@linkplain FieldMap#getSection()
474      * section} in this container; each field map is stored as child element of
475      * section it is in. Otherwise (if <code>level2Name</code> is
476      * <code>null</code>) all field maps are stored as direct child elements of
477      * the main element, without using a medium level.
478      *
479      * @param name the qualified name to use for the main element
480      * @param level2Name the name of elements inserted as medium level;
481      * or <code>null</code> if no medium level should be used
482      * @return the created XML element
483      */
484     public ObjectElement toElement(final QName name, final QName level2Name) {
485         // create element of specified type
486         final ObjectElement result = new ObjectElement(name, this.getClass());
487         Element level2Element = null;
488         int currentSection = -1;
489 
490         final Iterator<FieldMap> entryIter = entryIterator();
491         FieldMap fieldMap;
492         Element childElement;
493 
494         while (entryIter.hasNext()) {
495             fieldMap = entryIter.next();
496             // java attribute is omitted
497             childElement = fieldMap.toElement(false);
498 
499             if ((level2Name != null)
500                     && (currentSection != fieldMap.getSection())) {
501                 // section number has changed: create new level 2 element
502                 level2Element = new DefaultElement(level2Name);
503                 result.add(level2Element);
504                 currentSection = fieldMap.getSection();
505             }
506 
507             // ignore null elements (empty field maps)
508             if (childElement != null) {
509                 if (level2Element != null) {
510                     // add as child of level 2 element, if used
511                     level2Element.add(childElement);
512                 } else {
513                     // otherwise add as child to main element
514                     result.add(childElement);
515                 }
516             }
517         }
518 
519         return result;
520     }
521 
522     /***
523      * Stores all contents of this container in an XML element for
524      * serialization. All field maps stored in this container are added as child
525      * elements.
526      *
527      * @param localName the local name to use for the main element, will be
528      * converted into a qualified name by calling
529      * {@link DOMUtils#defaultName(String)}
530      * @return the created XML element
531      */
532     public ObjectElement toElement(final String localName) {
533         return toElement(DOMUtils.defaultName(localName));
534     }
535 
536     /***
537      * Returns a string representation of this object.
538      *
539      * @return a textual representation
540      */
541     public String toString() {
542         return new ToStringBuilder(this)
543             .append("key set", keySet)
544             .toString();
545     }
546 
547 }