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.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
143 while (outerIter.hasNext()) {
144 childElement = (Element) outerIter.next();
145 innerIter = childElement.elementIterator();
146
147 if (innerIter.hasNext()) {
148
149
150 sectionNo++;
151
152 while (innerIter.hasNext()) {
153
154 add(new FieldMap((Element) innerIter.next(), sectionNo));
155 }
156 } else {
157
158 if (sectionNo < 0) {
159 sectionNo = 0;
160 }
161
162
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
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
225 if (!backgroundMap.isEmpty()) {
226 map.putAll(backgroundMap);
227 }
228
229 entries.add(map);
230
231 if (checkKeys) {
232
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
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
266 currentKey = keyIter.next();
267 currentValue = valueIter.next();
268
269
270 if ((currentValue != null) && (!"".equals(currentValue))) {
271 fieldMap.put(currentKey, currentValue);
272 }
273 }
274
275
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
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
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
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
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
497 childElement = fieldMap.toElement(false);
498
499 if ((level2Name != null)
500 && (currentSection != fieldMap.getSection())) {
501
502 level2Element = new DefaultElement(level2Name);
503 result.add(level2Element);
504 currentSection = fieldMap.getSection();
505 }
506
507
508 if (childElement != null) {
509 if (level2Element != null) {
510
511 level2Element.add(childElement);
512 } else {
513
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 }