001/*
002 * Copyright 2005-2008 The Kuali Foundation
003 * 
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * 
008 * http://www.opensource.org/licenses/ecl2.php
009 * 
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.kns.util.properties;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.Iterator;
022import java.util.LinkedHashMap;
023import java.util.LinkedHashSet;
024import java.util.Map;
025import java.util.Properties;
026import java.util.Set;
027
028import org.apache.commons.lang.StringUtils;
029import org.apache.log4j.Logger;
030
031/**
032 * This class is a Recursive container for single- and multi-level key,value pairs. It relies on the assumption that the consumer
033 * (presumably a JSP) will (implicitly) call toString at the end of the chain, which will return the String value of the chain's
034 * endpoint.
035 * 
036 * It implements Map because that's how we fool jstl into converting "a.b.c" into get("a").get("b").get("c") instead of
037 * getA().getB().getC()
038 * 
039 * Uses LinkedHashMap and LinkedHashSet because iteration order is now important.
040 * 
041 * 
042 */
043public class PropertyTree implements Map {
044    private static Logger LOG = Logger.getLogger(PropertyTree.class);
045
046    final boolean flat;
047    final PropertyTree parent;
048    String directValue;
049    Map children;
050
051    /**
052     * Creates an empty instance with no parent
053     */
054    public PropertyTree() {
055        this(false);
056    }
057
058    /**
059     * Creates an empty instance with no parent. If flat is true, entrySet and size and the iterators will ignore entries in
060     * subtrees.
061     */
062    public PropertyTree(boolean flat) {
063        this.parent = null;
064        this.children = new LinkedHashMap();
065        this.flat = flat;
066    }
067
068    /**
069     * Creates an empty instance with the given parent. If flat is true, entrySet and size and the iterators will ignore entries in
070     * subtrees.
071     */
072    private PropertyTree(PropertyTree parent) {
073        this.parent = parent;
074        this.children = new LinkedHashMap();
075        this.flat = parent.flat;
076    }
077
078    /**
079     * Associates the given key with the given value. If the given key has multiple levels (consists of multiple strings separated
080     * by '.'), the property value is stored such that it can be retrieved either directly, by calling get() and passing the entire
081     * key; or indirectly, by decomposing the key into its separate levels and calling get() successively on the result of the
082     * previous level's get. <br>
083     * For example, given <br>
084     * <code>
085     * PropertyTree tree = new PropertyTree();
086     * tree.set( "a.b.c", "something" );
087     * </code> the following statements are
088     * equivalent ways to retrieve the value: <br>
089     * <code>
090     * Object one = tree.get( "a.b.c" );
091     * </code>
092     * <code>
093     * Object two = tree.get( "a" ).get( "b" ).get( "c" );
094     * </code><br>
095     * Note: since I can't have the get method return both a PropertyTree and a String, getting an actual String requires calling
096     * toString on the PropertyTree returned by get.
097     * 
098     * @param key
099     * @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}