View Javadoc

1   // BSD License (http://www.galagosearch.org/license)
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             // this key may actually be a path expression.
196             // if so, we consider just the first part, and call that the key
197             String[] fields = key.split("/", 2);
198             key = fields[0];
199 
200             // get the appropriate list from the map
201             List<Value> list = _map.get(key);
202 
203             // if it's not a path, just return what we found.
204             if (fields.length == 1) {
205                 return list;
206             } else {
207                 // it's a path, so we descend through the first
208                 // item of this list, then ask for the list corresponding
209                 // to the rest of this path
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             // if there are no variables in there, store as a String
343             if (current.isEmpty()) {
344                 if (writer.isStatic()) {
345                     current.set(writer.toString());
346                 } else {
347                     current.set(writer);            // make a new sequence
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      * &gt;a&lt;&gt;b&lt;&gt;c&ltd&gt;/c&lt;&gt;/b&lt;&gt;/a&lt;
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                 // this is a command-line argument, not a parameters file
438                 int startIndex = 1;
439 
440                 // skip any number of leading dashes
441                 while (arg.length() > startIndex && arg.charAt(startIndex) == '-') {
442                     startIndex++;                // split on equals (format is --argument=value, or just --argument)
443                 }
444                 String[] fields = arg.substring(startIndex).split("=");
445                 // on the command line, we allow either slashes or dots as the key;
446                 // like --corpus/path=collection or --corpus.path=collection,
447                 // but internal code requires slashes.
448                 String key = fields[0].replace('.', '/');
449                 String value;
450 
451                 // if there's no explicit value, assume they just mean 'true'
452                 if (fields.length == 1) {
453                     value = "True";
454                 } else {
455                     value = fields[1];
456                 }
457 
458                 _data.add(key, value);
459             } else {
460                 // it's a file, so parse it
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             // True, true, Yes, yes, non-zero
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 }