View Javadoc
1   /*
2    * The Kuali Financial System, a comprehensive financial management system for higher education.
3    * 
4    * Copyright 2005-2014 The Kuali Foundation
5    * 
6    * This program is free software: you can redistribute it and/or modify
7    * it under the terms of the GNU Affero General Public License as
8    * published by the Free Software Foundation, either version 3 of the
9    * License, or (at your option) any later version.
10   * 
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   * GNU Affero General Public License for more details.
15   * 
16   * You should have received a copy of the GNU Affero General Public License
17   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18   */
19  package org.kuali.rice.kns.util.properties;
20  
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.Iterator;
25  import java.util.LinkedHashMap;
26  import java.util.LinkedHashSet;
27  import java.util.Map;
28  import java.util.Properties;
29  import java.util.Set;
30  
31  import org.apache.commons.lang.StringUtils;
32  import org.apache.log4j.Logger;
33  
34  /**
35   * This class is a Recursive container for single- and multi-level key,value pairs. It relies on the assumption that the consumer
36   * (presumably a JSP) will (implicitly) call toString at the end of the chain, which will return the String value of the chain's
37   * endpoint.
38   * 
39   * It implements Map because that's how we fool jstl into converting "a.b.c" into get("a").get("b").get("c") instead of
40   * getA().getB().getC()
41   * 
42   * Uses LinkedHashMap and LinkedHashSet because iteration order is now important.
43   * 
44   * 
45   */
46  public class PropertyTree implements Map {
47      private static Logger LOG = Logger.getLogger(PropertyTree.class);
48  
49      final boolean flat;
50      final PropertyTree parent;
51      String directValue;
52      Map children;
53  
54      /**
55       * Creates an empty instance with no parent
56       */
57      public PropertyTree() {
58          this(false);
59      }
60  
61      /**
62       * Creates an empty instance with no parent. If flat is true, entrySet and size and the iterators will ignore entries in
63       * subtrees.
64       */
65      public PropertyTree(boolean flat) {
66          this.parent = null;
67          this.children = new LinkedHashMap();
68          this.flat = flat;
69      }
70  
71      /**
72       * Creates an empty instance with the given parent. If flat is true, entrySet and size and the iterators will ignore entries in
73       * subtrees.
74       */
75      private PropertyTree(PropertyTree parent) {
76          this.parent = parent;
77          this.children = new LinkedHashMap();
78          this.flat = parent.flat;
79      }
80  
81      /**
82       * Associates the given key with the given value. If the given key has multiple levels (consists of multiple strings separated
83       * by '.'), the property value is stored such that it can be retrieved either directly, by calling get() and passing the entire
84       * key; or indirectly, by decomposing the key into its separate levels and calling get() successively on the result of the
85       * previous level's get. <br>
86       * For example, given <br>
87       * <code>
88       * PropertyTree tree = new PropertyTree();
89       * tree.set( "a.b.c", "something" );
90       * </code> the following statements are
91       * equivalent ways to retrieve the value: <br>
92       * <code>
93       * Object one = tree.get( "a.b.c" );
94       * </code>
95       * <code>
96       * Object two = tree.get( "a" ).get( "b" ).get( "c" );
97       * </code><br>
98       * Note: since I can't have the get method return both a PropertyTree and a String, getting an actual String requires calling
99       * toString on the PropertyTree returned by get.
100      * 
101      * @param key
102      * @param value
103      * @throws IllegalArgumentException if the key is null
104      * @throws IllegalArgumentException if the value is null
105      */
106     public void setProperty(String key, String value) {
107         validateKey(key);
108         validateValue(value);
109 
110         if (parent == null) {
111             LOG.debug("setting (k,v) (" + key + "," + value + ")");
112         }
113 
114         if (StringUtils.contains(key, '.')) {
115             String prefix = StringUtils.substringBefore(key, ".");
116             String suffix = StringUtils.substringAfter(key, ".");
117 
118             PropertyTree node = getChild(prefix);
119             node.setProperty(suffix, value);
120         }
121         else {
122             PropertyTree node = getChild(key);
123             node.setDirectValue(value);
124         }
125     }
126 
127     /**
128      * Inserts all properties from the given Properties instance into this PropertyTree.
129      * 
130      * @param properties
131      * @throws IllegalArgumentException if the Properties object is null
132      * @throws IllegalArgumentException if a property's key is null
133      * @throws IllegalArgumentException if a property's value is null
134      */
135     public void setProperties(Properties properties) {
136         if (properties == null) {
137             throw new IllegalArgumentException("invalid (null) Properties object");
138         }
139 
140         for (Iterator i = properties.entrySet().iterator(); i.hasNext();) {
141             Map.Entry e = (Map.Entry) i.next();
142             setProperty((String) e.getKey(), (String) e.getValue());
143         }
144     }
145 
146     public void setProperties(Map<String,String> properties) {
147         if (properties == null) {
148             throw new IllegalArgumentException("invalid (null) Properties object");
149         }
150 
151         for (Iterator i = properties.entrySet().iterator(); i.hasNext();) {
152             Map.Entry e = (Map.Entry) i.next();
153             setProperty((String) e.getKey(), (String) e.getValue());
154         }
155     }
156     
157     /**
158      * Returns the PropertyTree object with the given key, or null if there is none.
159      * 
160      * @param key
161      * @return
162      * @throws IllegalArgumentException if the key is null
163      */
164     private PropertyTree getSubtree(String key) {
165         validateKey(key);
166 
167         PropertyTree returnValue = null;
168         if (StringUtils.contains(key, '.')) {
169             String prefix = StringUtils.substringBefore(key, ".");
170             String suffix = StringUtils.substringAfter(key, ".");
171 
172             PropertyTree child = (PropertyTree) this.children.get(prefix);
173             if (child != null) {
174                 returnValue = child.getSubtree(suffix);
175             }
176         }
177         else {
178             returnValue = (PropertyTree) this.children.get(key);
179         }
180 
181         return returnValue;
182     }
183 
184 
185     /**
186      * @param key
187      * @return the directValue of the PropertyTree associated with the given key, or null if there is none
188      */
189     public String getProperty(String key) {
190         String propertyValue = null;
191 
192         PropertyTree subtree = getSubtree(key);
193         if (subtree != null) {
194             propertyValue = subtree.getDirectValue();
195         }
196 
197         return propertyValue;
198     }
199 
200 
201     /**
202      * @return an unmodifiable copy of the direct children of this PropertyTree
203      */
204     public Map getDirectChildren() {
205         return Collections.unmodifiableMap(this.children);
206     }
207 
208 
209     /**
210      * Returns the directValue of this PropertyTree, or null if there is none.
211      * <p>
212      * This is the hack that makes it possible for jstl to get what it needs when trying to retrive the value of a simple key or of
213      * a complex (multi-part) key.
214      */
215     public String toString() {
216         return getDirectValue();
217     }
218 
219     /**
220      * Sets the directValue of this PropertyTree to the given value.
221      * 
222      * @param value
223      */
224     private void setDirectValue(String value) {
225         validateValue(value);
226 
227         this.directValue = value;
228     }
229 
230     /**
231      * @return directValue of this PropertyTree, or null if there is none
232      */
233     private String getDirectValue() {
234         return this.directValue;
235     }
236 
237     /**
238      * @return true if the directValue of this PropertyTree is not null
239      */
240     private boolean hasDirectValue() {
241         return (this.directValue != null);
242     }
243 
244     /**
245      * @return true if the this PropertyTree has children
246      */
247     private boolean hasChildren() {
248         return (!this.children.isEmpty());
249     }
250 
251     /**
252      * Returns the PropertyTree associated with the given key. If none exists, creates a new PropertyTree associates it with the
253      * given key, and returns it.
254      * 
255      * @param key
256      * @return PropertyTree associated with the given key
257      * @throws IllegalArgumentException if the given key is null
258      */
259     private PropertyTree getChild(String key) {
260         validateKey(key);
261 
262         PropertyTree child = (PropertyTree) this.children.get(key);
263         if (child == null) {
264             child = new PropertyTree((PropertyTree)this);
265             this.children.put(key, child);
266         }
267 
268         return child;
269     }
270 
271     /**
272      * @param key
273      * @throws IllegalArgumentException if the given key is not a String, or is null
274      */
275     private void validateKey(Object key) {
276         if (!(key instanceof String)) {
277             throw new IllegalArgumentException("invalid (non-String) key");
278         }
279         else if (key == null) {
280             throw new IllegalArgumentException("invalid (null) key");
281         }
282     }
283 
284     /**
285      * @param value
286      * @throws IllegalArgumentException if the given value is not a String, or is null
287      */
288     private void validateValue(Object value) {
289         if (!(value instanceof String)) {
290             throw new IllegalArgumentException("invalid (non-String) value");
291         }
292         else if (value == null) {
293             throw new IllegalArgumentException("invalid (null) value");
294         }
295     }
296 
297 
298     // Map methods
299     /**
300      * Returns an unmodifiable Set containing all key,value pairs in this PropertyTree and its children.
301      * 
302      * @see java.util.Map#entrySet()
303      */
304     public Set entrySet() {
305         return Collections.unmodifiableSet(collectEntries(null, this.flat).entrySet());
306     }
307 
308     /**
309      * Builds a HashMap containing all of the key,value pairs stored in this PropertyTree
310      * 
311      * @return
312      */
313     private Map collectEntries(String prefix, boolean flattenEntries) {
314         LinkedHashMap entryMap = new LinkedHashMap();
315 
316         for (Iterator i = this.children.entrySet().iterator(); i.hasNext();) {
317             Map.Entry e = (Map.Entry) i.next();
318             PropertyTree child = (PropertyTree) e.getValue();
319             String childKey = (String) e.getKey();
320 
321             // handle children with values
322             if (child.hasDirectValue()) {
323                 String entryKey = (prefix == null) ? childKey : prefix + "." + childKey;
324                 String entryValue = child.getDirectValue();
325 
326                 entryMap.put(entryKey, entryValue);
327             }
328 
329             // handle children with children
330             if (!flattenEntries && child.hasChildren()) {
331                 String childPrefix = (prefix == null) ? childKey : prefix + "." + childKey;
332 
333                 entryMap.putAll(child.collectEntries(childPrefix, flattenEntries));
334             }
335         }
336 
337         return entryMap;
338     }
339 
340     /**
341      * @return the number of keys contained, directly or indirectly, in this PropertyTree
342      */
343     public int size() {
344         return entrySet().size();
345     }
346 
347     /**
348      * @see java.util.Map#isEmpty()
349      */
350     public boolean isEmpty() {
351         return entrySet().isEmpty();
352     }
353 
354     /**
355      * Returns an unmodifiable Collection containing the values of all of the entries of this PropertyTree.
356      * 
357      * @see java.util.Map#values()
358      */
359     public Collection values() {
360         ArrayList values = new ArrayList();
361 
362         Set entrySet = entrySet();
363         for (Iterator i = entrySet.iterator(); i.hasNext();) {
364             Map.Entry e = (Map.Entry) i.next();
365 
366             values.add(e.getValue());
367         }
368 
369         return Collections.unmodifiableList(values);
370     }
371 
372     /**
373      * Returns an unmodifiable Set containing the keys of all of the entries of this PropertyTree.
374      * 
375      * @see java.util.Map#keySet()
376      */
377     public Set keySet() {
378         LinkedHashSet keys = new LinkedHashSet();
379 
380         Set entrySet = entrySet();
381         for (Iterator i = entrySet.iterator(); i.hasNext();) {
382             Map.Entry e = (Map.Entry) i.next();
383 
384             keys.add(e.getKey());
385         }
386 
387         return Collections.unmodifiableSet(keys);
388     }
389 
390     /**
391      * @see java.util.Map#containsKey(java.lang.Object)
392      */
393     public boolean containsKey(Object key) {
394         validateKey(key);
395 
396         boolean containsKey = false;
397 
398         Set entrySet = entrySet();
399         for (Iterator i = entrySet.iterator(); !containsKey && i.hasNext();) {
400             Map.Entry e = (Map.Entry) i.next();
401 
402             Object entryKey = e.getKey();
403             containsKey = (entryKey != null) && entryKey.equals(key);
404         }
405 
406         return containsKey;
407     }
408 
409     /**
410      * @see java.util.Map#containsValue(java.lang.Object)
411      */
412     public boolean containsValue(Object value) {
413         validateValue(value);
414 
415         boolean containsValue = false;
416 
417         Set entrySet = entrySet();
418         for (Iterator i = entrySet.iterator(); !containsValue && i.hasNext();) {
419             Map.Entry e = (Map.Entry) i.next();
420 
421             Object entryValue = e.getValue();
422             containsValue = (entryValue != null) && entryValue.equals(value);
423         }
424 
425         return containsValue;
426     }
427 
428     /**
429      * Traverses the tree structure until it finds the PropertyTree pointed to by the given key, and returns that PropertyTree
430      * instance.
431      * <p>
432      * Only returns PropertyTree instances; if you want the String value pointed to by a given key, you must call toString() on the
433      * returned PropertyTree (after verifying that it isn't null, of course).
434      * 
435      * @see java.util.Map#get(java.lang.Object)
436      */
437     public Object get(Object key) {
438         validateKey(key);
439 
440         return getSubtree((String) key);
441     }
442 
443 
444     // unsupported operations
445     /**
446      * Unsupported, since you can't change the contents of a PropertyTree once it has been initialized.
447      */
448     public void clear() {
449         throw new UnsupportedOperationException();
450     }
451 
452     /**
453      * Unsupported, since you can't change the contents of a PropertyTree once it has been initialized.
454      */
455     public void putAll(Map t) {
456         throw new UnsupportedOperationException();
457     }
458 
459     /**
460      * Unsupported, since you can't change the contents of a PropertyTree once it has been initialized.
461      */
462     public Object remove(Object key) {
463         throw new UnsupportedOperationException();
464     }
465 
466     /**
467      * Unsupported, since you can't change the contents of a PropertyTree once it has been initialized.
468      */
469     public Object put(Object key, Object value) {
470         throw new UnsupportedOperationException();
471     }
472 }