View Javadoc

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