1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
93 final InputStream uncompressedStream =
94 IOUtils.openCompressableInStream(in);
95
96
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
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
216 if (!backgroundMap.isEmpty()) {
217 map.putAll(backgroundMap);
218 }
219
220 entries.add(map);
221
222 if (checkKeys) {
223
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
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
254 currentKey = keyIter.next();
255 currentValue = valueIter.next();
256
257
258 if ((currentValue != null) && (!"".equals(currentValue))) {
259 fieldMap.put(currentKey, currentValue);
260 }
261 }
262
263
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
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
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 }