001/*
002 * The Kuali Financial System, a comprehensive financial management system for higher education.
003 * 
004 * Copyright 2005-2014 The Kuali Foundation
005 * 
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU Affero General Public License as
008 * published by the Free Software Foundation, either version 3 of the
009 * License, or (at your option) any later version.
010 * 
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU Affero General Public License for more details.
015 * 
016 * You should have received a copy of the GNU Affero General Public License
017 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
018 */
019package org.kuali.rice.kns.util.properties;
020
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.Iterator;
025import java.util.LinkedHashMap;
026import java.util.LinkedHashSet;
027import java.util.Map;
028import java.util.Properties;
029import java.util.Set;
030
031import org.apache.commons.lang.StringUtils;
032import org.apache.log4j.Logger;
033
034/**
035 * This class is a Recursive container for single- and multi-level key,value pairs. It relies on the assumption that the consumer
036 * (presumably a JSP) will (implicitly) call toString at the end of the chain, which will return the String value of the chain's
037 * endpoint.
038 * 
039 * It implements Map because that's how we fool jstl into converting "a.b.c" into get("a").get("b").get("c") instead of
040 * getA().getB().getC()
041 * 
042 * Uses LinkedHashMap and LinkedHashSet because iteration order is now important.
043 * 
044 * 
045 */
046public class PropertyTree implements Map {
047    private static Logger LOG = Logger.getLogger(PropertyTree.class);
048
049    final boolean flat;
050    final PropertyTree parent;
051    String directValue;
052    Map children;
053
054    /**
055     * Creates an empty instance with no parent
056     */
057    public PropertyTree() {
058        this(false);
059    }
060
061    /**
062     * Creates an empty instance with no parent. If flat is true, entrySet and size and the iterators will ignore entries in
063     * subtrees.
064     */
065    public PropertyTree(boolean flat) {
066        this.parent = null;
067        this.children = new LinkedHashMap();
068        this.flat = flat;
069    }
070
071    /**
072     * Creates an empty instance with the given parent. If flat is true, entrySet and size and the iterators will ignore entries in
073     * subtrees.
074     */
075    private PropertyTree(PropertyTree parent) {
076        this.parent = parent;
077        this.children = new LinkedHashMap();
078        this.flat = parent.flat;
079    }
080
081    /**
082     * Associates the given key with the given value. If the given key has multiple levels (consists of multiple strings separated
083     * by '.'), the property value is stored such that it can be retrieved either directly, by calling get() and passing the entire
084     * key; or indirectly, by decomposing the key into its separate levels and calling get() successively on the result of the
085     * previous level's get. <br>
086     * For example, given <br>
087     * <code>
088     * PropertyTree tree = new PropertyTree();
089     * tree.set( "a.b.c", "something" );
090     * </code> the following statements are
091     * equivalent ways to retrieve the value: <br>
092     * <code>
093     * Object one = tree.get( "a.b.c" );
094     * </code>
095     * <code>
096     * Object two = tree.get( "a" ).get( "b" ).get( "c" );
097     * </code><br>
098     * Note: since I can't have the get method return both a PropertyTree and a String, getting an actual String requires calling
099     * 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}