1
2 package org.galagosearch.tupleflow;
3
4 import java.io.File;
5 import java.io.IOException;
6 import java.io.Serializable;
7 import java.io.StringReader;
8 import java.io.StringWriter;
9 import java.util.ArrayList;
10 import java.util.Collections;
11 import java.util.HashMap;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Stack;
15 import javax.xml.parsers.DocumentBuilderFactory;
16 import javax.xml.parsers.SAXParser;
17 import javax.xml.parsers.SAXParserFactory;
18 import javax.xml.transform.Transformer;
19 import javax.xml.transform.TransformerFactory;
20 import javax.xml.transform.dom.DOMSource;
21 import javax.xml.transform.stream.StreamResult;
22 import org.w3c.dom.Document;
23 import org.w3c.dom.Element;
24 import org.xml.sax.Attributes;
25 import org.xml.sax.InputSource;
26 import org.xml.sax.SAXException;
27 import org.xml.sax.helpers.DefaultHandler;
28
29 /***
30 * A Parameters object is a hierarchical collection of strings. TupleFlow
31 * uses it as a convenient way to pass parameters to objects, read and write
32 * parameters from files, and read parameters from the command line.
33 *
34 * @author trevor
35 */
36 public class Parameters implements Serializable {
37 public static class Variable implements Serializable {
38 String name;
39
40 public Variable(String name) {
41 this.name = name;
42 }
43
44 public String toString(HashMap<String, String> values) {
45 return values.get(name);
46 }
47 }
48
49 public static class Value implements Serializable {
50 Map<String, List<Value>> _map;
51 CharSequence _string;
52
53 /***
54 * Construct a new Value object with nothing in it.
55 */
56 public Value() {
57 _map = null;
58 _string = "";
59 }
60
61 /***
62 * Returns true if there is no data in this value object.
63 */
64 public boolean isEmpty() {
65 return _map == null && _string.equals("");
66 }
67
68 /***
69 * Creates the map if it is currently null.
70 */
71 private void ensureMap() {
72 if (_map == null) {
73 _map = new HashMap<String, List<Value>>();
74 }
75 }
76
77 /***
78 * Ensures that the map exists. Adds a values array to
79 * the map if there isn't one already. Returns the values
80 * array corresponding to this key.
81 */
82 private List<Value> ensureKey(String key) {
83 ensureMap();
84 List<Value> values = new ArrayList<Value>();
85 if (!_map.containsKey(key)) {
86 _map.put(key, values);
87 } else {
88 values = _map.get(key);
89 }
90 return values;
91 }
92
93 /***
94 * Add a new child value object. This is similar to adding a
95 * child XML element at this point, but without actually putting any
96 * data in that element yet.
97 *
98 * @param key The XML tag/key name of this child value.
99 * @return A new empty child Value object.
100 */
101 public Value add(String key) {
102 Value result = new Value();
103 ensureKey(key).add(result);
104 return result;
105 }
106
107 public void add(String key, List<Value> values) {
108 if (key.contains("/")) {
109 String fields[] = key.split("/", 2);
110 String subKey = fields[1];
111 String rootKey = fields[0];
112 Value subValue = null;
113
114 if (!containsKey(rootKey)) {
115 subValue = add(rootKey);
116 } else {
117 subValue = list(rootKey).get(0);
118 }
119 subValue.add(subKey, values);
120 } else {
121 ensureKey(key).addAll(values);
122 }
123 }
124
125 /***
126 * Add a new XML value. Key may be a simple tag name
127 * or a slash-delimited XML pathname.
128 *
129 * @param key The XML path to the tag this call should modify/add.
130 * @param value The text value to assign to the node specified by the key parameter.
131 */
132 public void add(String key, CharSequence value) {
133 Value stringValue = new Value();
134 stringValue._string = value;
135 List<Value> valueList = Collections.singletonList(stringValue);
136
137 add(key, valueList);
138 }
139
140 public void set(CharSequence value) {
141 _map = null;
142 _string = value;
143 }
144
145 public void set(String key, List<Value> values) {
146 if (key.contains("/")) {
147 String fields[] = key.split("/", 2);
148 String subKey = fields[1];
149 String rootKey = fields[0];
150 Value subValue = null;
151
152 if (!containsKey(rootKey)) {
153 subValue = add(rootKey);
154 } else {
155 subValue = list(rootKey).get(0);
156 }
157 subValue.add(subKey, values);
158 } else {
159 List<Value> current = ensureKey(key);
160 current.clear();
161 current.addAll(values);
162 }
163 }
164
165 @Override
166 public String toString() {
167 return _string.toString();
168 }
169
170 public Map<String, List<Value>> map() {
171 return _map;
172 }
173
174 public String get(String key, String def) {
175 if (containsKey(key)) {
176 return get(key);
177 }
178 return def;
179 }
180
181 public String get(String key) {
182 if (key == null || key.length() == 0) {
183 return toString();
184 }
185 List<Value> list = list(key);
186
187 if (list == null) {
188 throw new IllegalArgumentException("Key '" + key + "' not found.");
189 }
190 Value first = list.get(0);
191 return first.toString();
192 }
193
194 public List<Value> list(String key) {
195
196
197 String[] fields = key.split("/", 2);
198 key = fields[0];
199
200
201 List<Value> list = _map.get(key);
202
203
204 if (fields.length == 1) {
205 return list;
206 } else {
207
208
209
210 String tail = fields[1];
211 return list.get(0).list(tail);
212 }
213 }
214
215 public List<String> stringList(String key) {
216 List<Value> list = list(key);
217 ArrayList<String> strings = new ArrayList<String>(list.size());
218
219 for (Value value : list) {
220 strings.add(value.toString());
221 }
222
223 return strings;
224 }
225
226 public boolean containsKey(String key) {
227 try {
228 get(key);
229 } catch (Exception e) {
230 return false;
231 }
232
233 return true;
234 }
235 }
236 Value _data = new Value();
237 HashMap<String, String> _variables = new HashMap<String, String>();
238
239 /***
240 * This class gathers up a stack of CharSequence objects and makes
241 * them look like a single CharSequence. The reason we do this is so
242 * that we can insert special, mutable CharSequences in here that are used
243 * as parameters.
244 *
245 * For example, suppose we have an input string like:
246 * ${path:/Users/trevor/Desktop}.txt
247 * We can make a CharSequenceBuffer = [ MutableCharSequence("path"), ".txt" ]
248 *
249 * Now, we can go and change the MutableCharSequence later so that parameters work
250 * appropriately.
251 */
252 public class CharSequenceBuffer implements CharSequence {
253 ArrayList sequences = new ArrayList();
254
255 public void add(Object sequence) {
256 sequences.add(sequence);
257 }
258
259 public boolean isStatic() {
260 for (Object sequence : sequences) {
261 boolean isString = (sequence instanceof String);
262 if (!isString) {
263 return false;
264 }
265 }
266
267 return true;
268 }
269
270 @Override
271 public String toString() {
272 StringBuilder builder = new StringBuilder();
273
274 for (Object sequence : sequences) {
275 builder.append(sequence.toString());
276 }
277
278 return builder.toString();
279 }
280
281 public String toString(HashMap<String, String> values) {
282 StringBuilder builder = new StringBuilder();
283
284 for (Object sequence : sequences) {
285 if (sequence instanceof Variable) {
286 Variable v = (Variable) sequence;
287 builder.append(_variables.get(v.name));
288 } else {
289 builder.append(sequence.toString());
290 }
291 }
292
293 return builder.toString();
294 }
295
296 public int length() {
297 return toString().length();
298 }
299
300 public char charAt(int index) {
301 return toString().charAt(index);
302 }
303
304 public CharSequence subSequence(int start, int end) {
305 return toString().subSequence(start, end);
306 }
307 }
308
309 public class Parser extends DefaultHandler {
310 CharSequenceBuffer writer = new CharSequenceBuffer();
311 Stack<Value> contexts = new Stack();
312 Value current;
313
314 @Override
315 public void characters(char[] data, int start, int length) throws SAXException {
316 writer.add(new String(data, start, length));
317 }
318
319 @Override
320 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
321 if (qName.equals("parameters") && current == null) {
322 contexts.push(_data);
323 current = _data;
324 } else if (current == null) {
325 throw new SAXException("Found an outermost tag that was not 'parameters': " + qName);
326 } else if (qName.equals("variable")) {
327 String variableName = attributes.getValue("name");
328 String variableDefault = attributes.getValue("default");
329 Variable variable = new Variable(variableName);
330
331 _variables.put(variableName, variableDefault);
332 writer.add(variable);
333 } else {
334 writer = new CharSequenceBuffer();
335 current = current.add(qName);
336 contexts.push(current);
337 }
338 }
339
340 @Override
341 public void endElement(String uri, String localName, String qName) throws SAXException {
342
343 if (current.isEmpty()) {
344 if (writer.isStatic()) {
345 current.set(writer.toString());
346 } else {
347 current.set(writer);
348 }
349 }
350 writer = new CharSequenceBuffer();
351 contexts.pop();
352
353 if (!contexts.empty()) {
354 current = contexts.peek();
355 } else {
356 current = null;
357 }
358 }
359 }
360
361 public Parameters() {
362 }
363
364 /***
365 * Creates a Parameters object using XML data.
366 *
367 * @param xmlData
368 * @throws java.io.IOException
369 */
370
371 public Parameters(byte[] xmlData) throws IOException {
372 parse(xmlData);
373 }
374
375 /***
376 * Creates a Parameters object using the contents of an XML file.
377 *
378 * @param f The file to grab XML data from.
379 * @throws java.io.IOException
380 */
381 public Parameters(File f) throws IOException {
382 parse(f.getPath());
383 }
384
385 public Parameters(Value v) {
386 _data = v;
387 }
388
389 /***
390 * Creates a Parameters object based on a key/value map.
391 *
392 * @param map
393 */
394 public Parameters(Map<String, String> map) {
395 _data = new Value();
396
397 for (String key : map.keySet()) {
398 _data.add(key, map.get(key));
399 }
400 }
401
402 /***
403 * <p>
404 * Fills in a Parameters object based on command line flags. The
405 * flag format is like this:
406 * </p>
407 *
408 * <pre>
409 * --a.b.c=d
410 * </pre>
411 *
412 * <p>
413 * This is equivalent to:
414 * <pre>
415 * >a<>b<>c<d>/c<>/b<>/a<
416 * </pre>
417 * </p>
418 *
419 * <p>A flag has no equals sign, like this one:</p>
420 * <pre>
421 * --a.b.c
422 * </pre>
423 * <p>is equivalent to:</p>
424 * <pre>
425 * --a.b.c=True
426 * </pre>
427 *
428 * <p>Any argument that doesn't begin with a dash is assumed to be the filename
429 * of an XML file. The data from that file data will be added to this object.</p>
430 *
431 * @param args
432 * @throws java.io.IOException
433 */
434 public Parameters(String[] args) throws IOException {
435 for (String arg : args) {
436 if (arg.startsWith("-")) {
437
438 int startIndex = 1;
439
440
441 while (arg.length() > startIndex && arg.charAt(startIndex) == '-') {
442 startIndex++;
443 }
444 String[] fields = arg.substring(startIndex).split("=");
445
446
447
448 String key = fields[0].replace('.', '/');
449 String value;
450
451
452 if (fields.length == 1) {
453 value = "True";
454 } else {
455 value = fields[1];
456 }
457
458 _data.add(key, value);
459 } else {
460
461 parse(arg);
462 }
463 }
464 }
465
466 public Parser getParseHandler() {
467 return new Parser();
468 }
469
470 /***
471 * Gets the value for this key. You can retrieve a nested value
472 * using a path syntax, e.g. get("a/b/c").
473 *
474 * @param key
475 */
476
477 public String get(String key) {
478 return _data.get(key);
479 }
480
481 /***
482 * Gets the value for this key. Returns def if key isn't found.
483 *
484 * @param key
485 * @param def
486 */
487
488 public String get(String key, String def) {
489 try {
490 return get(key);
491 } catch (Exception e) {
492 return def;
493 }
494 }
495
496 /***
497 * Gets the value for key. If key is not found, returns the
498 * value for "default". If "default" isn't found, returns def.
499 *
500 * @param key
501 * @param def
502 * @return
503 */
504
505 public String getAsDefault(String key, String def) {
506 return get(get(key), get("default", def));
507 }
508
509 /***
510 * Gets the value for this key, but returning a default value if the
511 * key doesn't exist in the object. This method tries to convert the
512 * value to boolean. Values starting with 'T', 'Y', or a non-zero number
513 * are considered to be true; everything else is false.
514 *
515 * @param key
516 * @param def
517 * @return
518 */
519 public boolean get(String key, boolean def) {
520 try {
521 String result = get(key);
522 char c = result.charAt(0);
523
524
525 if (c == 'T' || c == 't' || c == 'Y' || c == 'y' || (Character.isDigit(c) && c != '0')) {
526 return true;
527 }
528 return false;
529 } catch (Exception e) {
530 return def;
531 }
532 }
533
534 public boolean getAsDefault(String key, boolean def) {
535 return get(key, get("default", def));
536 }
537
538 public long get(String key, long def) {
539 try {
540 String result = get(key);
541 return Long.parseLong(result);
542 } catch (Exception e) {
543 return def;
544 }
545 }
546
547 public long getAsDefault(String key, long def) {
548 return get(key, get("default", def));
549 }
550
551 public double get(String key, double def) {
552 try {
553 String result = get(key);
554 return Double.parseDouble(result);
555 } catch (Exception e) {
556 return def;
557 }
558 }
559
560 public double getAsDefault(String key, double def) {
561 return get(get(key), get("default", def));
562 }
563
564 public void copy(String key, Parameters other) {
565 List<Value> values = other.list(key);
566
567 if (values == null) {
568 return;
569 }
570 if (_data.containsKey(key)) {
571 _data.list(key).addAll(values);
572 } else {
573 _data.add(key, values);
574 }
575 }
576
577 public void copy(Parameters other) {
578 if (other._data == null || other._data._map == null) {
579 return;
580 }
581 for (String key : other._data._map.keySet()) {
582 copy(key, other);
583 }
584 }
585
586 @Override
587 public Parameters clone() {
588 Parameters p = new Parameters();
589 p.copy(this);
590 return p;
591 }
592
593 public void add(String key, List<Value> values) {
594 _data.add(key, values);
595 }
596
597 public void add(String key, String value) {
598 _data.add(key, value);
599 }
600
601 public void set(String key, List<Value> values) {
602 _data.set(key, values);
603 }
604
605 public void set(String key, String value) {
606 Value stringValue = new Value();
607 stringValue.set(value);
608 List<Value> values = Collections.singletonList(stringValue);
609 _data.set(key, values);
610 }
611
612 public List<Value> list(String key) {
613 return _data.list(key);
614 }
615
616 public List<String> stringList(String key) {
617 return _data.stringList(key);
618 }
619
620 public Value value() {
621 return _data;
622 }
623
624 public boolean containsKey(String key) {
625 return _data.containsKey(key);
626 }
627
628 public void parse(String filename) throws IOException {
629 try {
630 SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
631 parser.parse(new File(filename), new Parser());
632 } catch (Exception e) {
633 throw new IOException(e.toString());
634 }
635 }
636
637 public void parse(byte[] xmlData) throws IOException {
638 try {
639 SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
640 String xmlText = new String(xmlData);
641 StringReader reader = new StringReader(xmlText);
642 parser.parse(new InputSource(reader), new Parser());
643 } catch (Exception e) {
644 throw new IOException(e.toString());
645 }
646 }
647
648 public void write(Value value, Document document, Element element) {
649 if (value._map == null) {
650 element.appendChild(document.createTextNode(value.toString()));
651 } else {
652 for (String key : value._map.keySet()) {
653 for (Value childValue : value._map.get(key)) {
654 Element childElement = document.createElement(key);
655 write(childValue, document, childElement);
656 element.appendChild(childElement);
657 }
658 }
659 }
660 }
661
662 public void write(StreamResult result) throws IOException {
663 try {
664 Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().
665 newDocument();
666 Element root = document.createElement("parameters");
667 write(_data, document, root);
668 document.appendChild(root);
669
670 Transformer identity = TransformerFactory.newInstance().newTransformer();
671 identity.transform(new DOMSource(document), result);
672 } catch (Exception e) {
673 throw new IOException(e.toString());
674 }
675 }
676
677 public void write(String filename) throws IOException {
678 try {
679 write(new StreamResult(new File(filename)));
680 } catch (Exception e) {
681 throw new IOException(e.toString());
682 }
683 }
684
685 @Override
686 public String toString() {
687 StringWriter writer = new StringWriter();
688 try {
689 write(new StreamResult(writer));
690 } catch (Exception e) {
691 return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- Exception: " + e.toString() + " --><parameters/>\n";
692 }
693 String result = writer.toString();
694 return result;
695 }
696
697 public boolean isEmpty() {
698 return _data.isEmpty();
699 }
700 }