View Javadoc

1   /*
2    * Copyright (C) 2004 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 library is free software; you can redistribute it and/or
8    * modify it under the terms of the GNU Lesser General Public
9    * License as published by the Free Software Foundation; either
10   * version 2.1 of the License, or (at your option) any later version.
11   *
12   * This library 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 GNU
15   * Lesser General Public License for more details.
16   *
17   * You should have received a copy of the GNU Lesser General Public
18   * License along with this library; if not, visit
19   * http://www.gnu.org/licenses/lgpl.html or write to the Free Software
20   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
21   */
22  package de.fu_berlin.ties.io;
23  
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.InputStreamReader;
27  import java.io.OutputStream;
28  import java.io.OutputStreamWriter;
29  import java.io.Reader;
30  import java.io.Writer;
31  import java.util.Iterator;
32  import java.util.LinkedHashMap;
33  import java.util.LinkedHashSet;
34  import java.util.LinkedList;
35  import java.util.List;
36  
37  import org.apache.commons.lang.builder.ToStringBuilder;
38  
39  import de.fu_berlin.ties.text.TextUtils;
40  
41  /***
42   * A container of {@link de.fu_berlin.ties.io.FieldMap}s. A container stores all
43   * field maps added to itself and keeps a set of all keys found in the field
44   * maps. This is an "append-only" container; field maps can only be added but
45   * never removed.
46   *
47   * <p>Instances of this class are not thread-safe and must be synchronized
48   * externally, if required.
49   *
50   * @author Christian Siefkes
51   * @version $Revision: 1.16 $, $Date: 2004/11/09 10:11:29 $, $Author: siefkes $
52   */
53  public class FieldContainer {
54  
55      /***
56       * Factory method that creates a field container in {@link DelimSepValues}
57       * format.
58       *
59       * @return the created container
60       */
61      public static FieldContainer createFieldContainer() {
62          return new DelimSepValues();
63      }
64  
65      /***
66       * Factory method that creates a field container from serialized data in
67       * {@link DelimSepValues} format.
68       *
69       * @param input the input data to process
70       * @throws IllegalArgumentException if the input data contains errors
71       * @return the created container
72       */
73      public static FieldContainer createFieldContainer(
74              final CharSequence input) throws IllegalArgumentException {
75          return new DelimSepValues(input);
76      }
77  
78      /***
79       * Factory method that creates a field container from serialized data in
80       * {@link DelimSepValues} format. Internally uses
81       * {@link IOUtils#openCompressableInStream(InputStream)} so
82       * <code>gzip</code>-compressed streams are also supported.
83       *
84       * @param in a stream containing the input data to process, must use the
85       * UTF-8 charset; the stream is not closed by this method
86       * @throws IOException if an I/O error occurs while reading from the stream
87       * @throws IllegalArgumentException if the input data contains errors
88       * @return the created container
89       */
90      public static FieldContainer createFieldContainer(final InputStream in)
91              throws IOException, IllegalArgumentException {
92          // autodetect compression if used
93          final InputStream uncompressedStream =
94              IOUtils.openCompressableInStream(in);
95  
96          // read data in UTF-8 charset
97          return createFieldContainer(new InputStreamReader(uncompressedStream,
98                  IOUtils.STANDARD_UNICODE_CHARSET));
99      }
100 
101     /***
102      * Factory method that creates a field container from serialized data in
103      * {@link DelimSepValues} format.
104      *
105      * @param reader a reader containing the input data to process; not closed
106      * by this method
107      * @throws IOException if an I/O error occurs while reading from the stream
108      * @throws IllegalArgumentException if the input data contains errors
109      * @return the created container
110      */
111     public static FieldContainer createFieldContainer(final Reader reader)
112             throws IOException, IllegalArgumentException {
113         return createFieldContainer(IOUtils.readToString(reader));
114     }
115 
116     /***
117      * Returns the file extension recommended for {@link FieldContainer}s.
118      *
119      * @return the recommended extension: {@link DelimSepValues#FILE_EXT}
120      */
121     public static String recommendedExtension() {
122         return DelimSepValues.FILE_EXT;
123     }
124 
125     /***
126      * This map can be used to extend newly added field maps: at each
127      * {@link #add(FieldMap)} operation, any key/value pairs from this map are
128      * added to field map prior to storing it. The map is empty by default.
129      */
130     private final FieldMap backgroundMap = new FieldMap();
131 
132     /***
133      * A map of attributes stored in this container.
134      */
135     private final FieldMap attributes = new FieldMap();
136 
137    /***
138     * A map of nested containers stored in this container.
139     */
140    private final LinkedHashMap<String, FieldContainer> nested =
141        new LinkedHashMap<String, FieldContainer>();
142 
143     /***
144      * A list of all the field maps stored in this container.
145      */
146     private final LinkedList<FieldMap> entries = new LinkedList<FieldMap>();
147 
148     /***
149      * A set containing all keys from the stored field maps.
150      */
151     private final LinkedHashSet<String> keySet = new LinkedHashSet<String>();
152 
153     /***
154      * Creates a new empty instance.
155      */
156     public FieldContainer() {
157         super();
158     }
159 
160     /***
161      * Creates a new instance and populates it from a {@link StorableContainer}.
162      *
163      * @param contents the contents to add by calling
164      * {@link StorableContainer#storeEntries(FieldContainer)}
165      */
166     public FieldContainer(final StorableContainer contents) {
167         this();
168         contents.storeEntries(this);
169     }
170 
171     /***
172      * Helper method that adds a key to the set of all keys.
173      *
174      * @param key the key to add
175      * @return <code>true</code> if the set did not already contain the
176      * specified element
177      */
178     protected boolean addKey(final String key) {
179         return keySet.add(key);
180     }
181 
182     /***
183      * Adds a field map to this container. Any new keys in this set are added
184      * at the end of the set of all keys, in the order of the field map's key
185      * set.
186      *
187      * @param map the field map to add
188      */
189     public void add(final FieldMap map) {
190         // we want to check keys
191         add(map, true);
192     }
193 
194     /***
195      * Adds the representation of a {@link Storable} to this container, by
196      * calling its {@link Storable#storeFields()} method and adding the
197      * resulting field map.
198      *
199      * @param storable the storable whose fields to add
200      */
201     public void add(final Storable storable) {
202         add(storable.storeFields());
203     }
204 
205     /***
206      * Helper method for adding a field map to this container.
207      *
208      * @param map the field map to add
209      * @param checkKeys whether to check the keys of the field map and add any
210      * missing keys to the set of all keys; subclasses calling this method can
211      * set this to <code>false</code> iff they know for sure that the map does
212      * not contain any new keys
213      */
214     protected void add(final FieldMap map, final boolean checkKeys) {
215         // add entries from the background map, if any
216         if (!backgroundMap.isEmpty()) {
217             map.putAll(backgroundMap);
218         }
219 
220         entries.add(map);
221 
222         if (checkKeys) {
223             // append any new keys at the end of the key set, in iteration order
224             keySet.addAll(map.keySet());
225         }
226     }
227 
228     /***
229      * Adds a field map created from the specified values, using the
230      * <em>n</em>-th key from the set of all keys for the <em>n</em>-th
231      * specified value. Empty strings and <code>null</code> values are omitted
232      * (the corresponding key is skipped).
233      *
234      * @param values the list of values to store in the field map
235      * @throws IllegalArgumentException if the specified array contains more
236      * elements than the set of all keys
237      */
238     public void add(final List values) throws IllegalArgumentException {
239         if (values.size() > keySet.size()) {
240             throw new IllegalArgumentException(
241                 "Cannot create FieldMap: more values (" + values.size()
242                 + ") than keys (" + keySet.size() + ")");
243         }
244 
245         // create and populate map
246         final FieldMap fieldMap = new FieldMap();
247         final Iterator<String> keyIter = keyIterator();
248         final Iterator valueIter = values.iterator();
249         String currentKey;
250         Object currentValue;
251 
252         while (valueIter.hasNext()) {
253             // we checked above that the key iterator has enough elements
254             currentKey = keyIter.next();
255             currentValue = valueIter.next();
256 
257             // skip null and empty strings
258             if ((currentValue != null) && (!"".equals(currentValue))) {
259                 fieldMap.put(currentKey, currentValue);
260             }
261         }
262 
263         // no need to check the keys, as they are taken from the set
264         add(fieldMap, false);
265     }
266 
267     /***
268      * Returns the number of attributes stored in this container.
269      *
270      * @return the number of attributes
271      */
272     public int attributeCount()  {
273         return attributes.size();
274     }
275 
276     /***
277      * Returns an iterator over the names of the attributes stored in this
278      * container.
279      *
280      * @return an iterator over the attribute names
281      */
282     public Iterator<String> attributeIterator() {
283         return attributes.keySet().iterator();
284     }
285 
286     /***
287      * This map can be used to extend newly added field maps: at each
288      * {@link #add(FieldMap)} operation, any key/value pairs from this map are
289      * added to field map prior to storing it. The map is empty by default.
290      *
291      * @return the background map
292      */
293     public FieldMap backgroundMap() {
294         return backgroundMap;
295     }
296 
297     /***
298      * Creates and returns a new nested subcontainer.
299      *
300      * @param name the name of the new subcontainer
301      * @return the newly created nested container
302      * @throws IllegalArgumentException if the specified name contains
303      * whitespace or is null or empty or if a nested container with the
304      * given name already exists
305      */
306     public FieldContainer createNestedContainer(final String name)
307     throws IllegalArgumentException {
308         TextUtils.ensurePrintableName(name);
309         if (nested.containsKey(name)) {
310             throw new IllegalArgumentException("Nested container " + name
311 					       + " already exists");
312 	}
313         final FieldContainer subContainer = new FieldContainer();
314         nested.put(name, subContainer);
315         return subContainer;
316     }
317 
318     /***
319      * Creates (deserializes) an list of objects of a specified type by calling
320      * {@link FieldMap#createObject(Class)} for each of the field maps contained
321      * in this container. The contained field maps are deserialized in the order
322      * they were added to this container.
323      *
324      * @param type the class of the objects to create; must have a constructor
325      * whose only argument is a <code>FieldMap</code>
326      * @return a list of the created object; all elements of this list will be
327      * instances of the specified class
328      * @throws InstantiationException if instantiation failed
329      * @throws SecurityException if access to the required reflection
330      * information is denied
331      */
332     public List<Object> createObjects(final Class type)
333     throws InstantiationException, SecurityException {
334         final List<Object> result = new LinkedList<Object>();
335         final Iterator entryIter = entryIterator();
336         FieldMap currentMap;
337 
338         // create and store an object from each field map
339         while (entryIter.hasNext()) {
340             currentMap = (FieldMap) entryIter.next();
341             result.add(currentMap.createObject(type));
342         }
343         return result;
344     }
345 
346     /***
347      * Returns an iterator over the {@link FieldMap}s in this container in the
348      * order they were added.
349      *
350      * @return an iterator over the contained field maps
351      */
352     public Iterator<FieldMap> entryIterator() {
353         return entries.iterator();
354     }
355 
356    /***
357      * Returns the value of an attribute.
358      *
359      * @param name the name of the attribute to look up
360      * @return the value of the attribute; or <code>null</code> if the
361      * attribute doesn't exist
362      */
363     public Object getAttribute(final String name) {
364         return attributes.get(name);
365     }
366 
367    /***
368     * Returns a nested subcontainer managed by this instance.
369     *
370     * @param name the name of the container to retrieve
371     * @return the nested container; or <code>null</code> if there is no
372     * container with this name
373     */
374     public FieldContainer getNestedContainer(final String name) {
375         return nested.get(name);
376     }
377 
378     /***
379      * Returns the number of keys in this container.
380      *
381      * @return the number of keys, i.e. the size of the set of all keys
382      */
383     public int keyCount() {
384         return keySet.size();
385     }
386 
387     /***
388      * Returns an iterator over the set of all keys used in contained field
389      * maps.
390      *
391      * @return an iterator over the set of all keys
392      */
393     public Iterator<String> keyIterator() {
394         return keySet.iterator();
395     }
396 
397    /***
398      * Returns the number of nested containers managed by container.
399      *
400      * @return the number of nested containers
401      */
402     public int nestedCount() {
403         return nested.size();
404     }
405    
406     /***
407      * Returns an iterator over the names of nested containers managed by this
408      * container.
409      *
410      * @return an iterator over the names of nested containers
411      */
412     public Iterator<String> nestedIterator() {
413         return nested.keySet().iterator();
414     }
415    
416     /***
417      * Returns the number of entries stored in this container.
418      *
419      * @return the number of entries
420      */
421     public int size() {
422         return entries.size();
423     }
424 
425     /***
426      * Serializes contents by wrapping the stream in a writer with UTF-8
427      * character set and delegating to {@link #store(Writer)}.
428      *
429      * @param out the output stream to write to; flushed but not closed by this
430      * method
431      * @throws IOException if an I/O error occurs while writing to the stream
432      */
433     public void store(final OutputStream out) throws IOException {
434         // wrap in writer with UTF-8 charset
435         store(new OutputStreamWriter(out, IOUtils.STANDARD_UNICODE_CHARSET));
436     }
437 
438     /***
439      * Sets name and value of an attribute.
440      *
441      * @param name the name of the attribute, must not contain whitespace
442      * @param value to value of the attribute
443      * @return the previous value associated with the specified attribute name,
444      * or <code>null</code> if there was no previous value
445      * @throws IllegalArgumentException if the specified name contains
446      * whitespace or is null or empty
447      */
448     public Object setAttribute(final String name, final Object value)
449     throws IllegalArgumentException {
450         TextUtils.ensurePrintableName(name);
451         return attributes.put(name, value);
452     }
453 
454     /***
455      * Subclasses can overwrite this method to serialize their contents
456      * in a class-specific format. This class does not prescribe a specific
457      * format and thus cannot store the data, throwing an
458      * {@link UnsupportedOperationException} instead.
459      *
460      * @param writer the writer to write to; not closed by this method
461      * @throws IOException might be thrown by subclasses if an I/O error occurs
462      * while serializing the data
463      * @throws UnsupportedOperationException always thrown by instances of this
464      * class
465      */
466     public void store(final Writer writer)
467             throws IOException, UnsupportedOperationException {
468         throw new UnsupportedOperationException(
469                 "Storing is not supported by FieldContainer");
470     }
471 
472     /***
473      * Returns a string representation of this object.
474      *
475      * @return a textual representation
476      */
477     public String toString() {
478         return new ToStringBuilder(this)
479             .append("key set", keySet)
480             .toString();
481     }
482 
483 }