/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 The Kuali Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kuali.rice.kns.util.properties; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Properties; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; /** * This class is a Recursive container for single- and multi-level key,value pairs. It relies on the assumption that the consumer * (presumably a JSP) will (implicitly) call toString at the end of the chain, which will return the String value of the chain's * endpoint. * * It implements Map because that's how we fool jstl into converting "a.b.c" into get("a").get("b").get("c") instead of * getA().getB().getC() * * Uses LinkedHashMap and LinkedHashSet because iteration order is now important. * * */ public class PropertyTree implements Map { private static Logger LOG = Logger.getLogger(PropertyTree.class); final boolean flat; final PropertyTree parent; String directValue; Map children; /** * Creates an empty instance with no parent */ public PropertyTree() { this(false); } /** * Creates an empty instance with no parent. If flat is true, entrySet and size and the iterators will ignore entries in * subtrees. */ public PropertyTree(boolean flat) { this.parent = null; this.children = new LinkedHashMap(); this.flat = flat; } /** * Creates an empty instance with the given parent. If flat is true, entrySet and size and the iterators will ignore entries in * subtrees. */ private PropertyTree(PropertyTree parent) { this.parent = parent; this.children = new LinkedHashMap(); this.flat = parent.flat; } /** * Associates the given key with the given value. If the given key has multiple levels (consists of multiple strings separated * by '.'), the property value is stored such that it can be retrieved either directly, by calling get() and passing the entire * key; or indirectly, by decomposing the key into its separate levels and calling get() successively on the result of the * previous level's get. <br> * For example, given <br> * <code> * PropertyTree tree = new PropertyTree(); * tree.set( "a.b.c", "something" ); * </code> the following statements are * equivalent ways to retrieve the value: <br> * <code> * Object one = tree.get( "a.b.c" ); * </code> * <code> * Object two = tree.get( "a" ).get( "b" ).get( "c" ); * </code><br> * Note: since I can't have the get method return both a PropertyTree and a String, getting an actual String requires calling * toString on the PropertyTree returned by get. * * @param key * @param value * @throws IllegalArgumentException if the key is null * @throws IllegalArgumentException if the value is null */ public void setProperty(String key, String value) { validateKey(key); validateValue(value); if (parent == null) { LOG.debug("setting (k,v) (" + key + "," + value + ")"); } if (StringUtils.contains(key, '.')) { String prefix = StringUtils.substringBefore(key, "."); String suffix = StringUtils.substringAfter(key, "."); PropertyTree node = getChild(prefix); node.setProperty(suffix, value); } else { PropertyTree node = getChild(key); node.setDirectValue(value); } } /** * Inserts all properties from the given Properties instance into this PropertyTree. * * @param properties * @throws IllegalArgumentException if the Properties object is null * @throws IllegalArgumentException if a property's key is null * @throws IllegalArgumentException if a property's value is null */ public void setProperties(Properties properties) { if (properties == null) { throw new IllegalArgumentException("invalid (null) Properties object"); } for (Iterator i = properties.entrySet().iterator(); i.hasNext();) { Map.Entry e = (Map.Entry) i.next(); setProperty((String) e.getKey(), (String) e.getValue()); } } public void setProperties(Map<String,String> properties) { if (properties == null) { throw new IllegalArgumentException("invalid (null) Properties object"); } for (Iterator i = properties.entrySet().iterator(); i.hasNext();) { Map.Entry e = (Map.Entry) i.next(); setProperty((String) e.getKey(), (String) e.getValue()); } } /** * Returns the PropertyTree object with the given key, or null if there is none. * * @param key * @return * @throws IllegalArgumentException if the key is null */ private PropertyTree getSubtree(String key) { validateKey(key); PropertyTree returnValue = null; if (StringUtils.contains(key, '.')) { String prefix = StringUtils.substringBefore(key, "."); String suffix = StringUtils.substringAfter(key, "."); PropertyTree child = (PropertyTree) this.children.get(prefix); if (child != null) { returnValue = child.getSubtree(suffix); } } else { returnValue = (PropertyTree) this.children.get(key); } return returnValue; } /** * @param key * @return the directValue of the PropertyTree associated with the given key, or null if there is none */ public String getProperty(String key) { String propertyValue = null; PropertyTree subtree = getSubtree(key); if (subtree != null) { propertyValue = subtree.getDirectValue(); } return propertyValue; } /** * @return an unmodifiable copy of the direct children of this PropertyTree */ public Map getDirectChildren() { return Collections.unmodifiableMap(this.children); } /** * Returns the directValue of this PropertyTree, or null if there is none. * <p> * 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 * a complex (multi-part) key. */ public String toString() { return getDirectValue(); } /** * Sets the directValue of this PropertyTree to the given value. * * @param value */ private void setDirectValue(String value) { validateValue(value); this.directValue = value; } /** * @return directValue of this PropertyTree, or null if there is none */ private String getDirectValue() { return this.directValue; } /** * @return true if the directValue of this PropertyTree is not null */ private boolean hasDirectValue() { return (this.directValue != null); } /** * @return true if the this PropertyTree has children */ private boolean hasChildren() { return (!this.children.isEmpty()); } /** * Returns the PropertyTree associated with the given key. If none exists, creates a new PropertyTree associates it with the * given key, and returns it. * * @param key * @return PropertyTree associated with the given key * @throws IllegalArgumentException if the given key is null */ private PropertyTree getChild(String key) { validateKey(key); PropertyTree child = (PropertyTree) this.children.get(key); if (child == null) { child = new PropertyTree((PropertyTree)this); this.children.put(key, child); } return child; } /** * @param key * @throws IllegalArgumentException if the given key is not a String, or is null */ private void validateKey(Object key) { if (!(key instanceof String)) { throw new IllegalArgumentException("invalid (non-String) key"); } else if (key == null) { throw new IllegalArgumentException("invalid (null) key"); } } /** * @param value * @throws IllegalArgumentException if the given value is not a String, or is null */ private void validateValue(Object value) { if (!(value instanceof String)) { throw new IllegalArgumentException("invalid (non-String) value"); } else if (value == null) { throw new IllegalArgumentException("invalid (null) value"); } } // Map methods /** * Returns an unmodifiable Set containing all key,value pairs in this PropertyTree and its children. * * @see java.util.Map#entrySet() */ public Set entrySet() { return Collections.unmodifiableSet(collectEntries(null, this.flat).entrySet()); } /** * Builds a HashMap containing all of the key,value pairs stored in this PropertyTree * * @return */ private Map collectEntries(String prefix, boolean flattenEntries) { LinkedHashMap entryMap = new LinkedHashMap(); for (Iterator i = this.children.entrySet().iterator(); i.hasNext();) { Map.Entry e = (Map.Entry) i.next(); PropertyTree child = (PropertyTree) e.getValue(); String childKey = (String) e.getKey(); // handle children with values if (child.hasDirectValue()) { String entryKey = (prefix == null) ? childKey : prefix + "." + childKey; String entryValue = child.getDirectValue(); entryMap.put(entryKey, entryValue); } // handle children with children if (!flattenEntries && child.hasChildren()) { String childPrefix = (prefix == null) ? childKey : prefix + "." + childKey; entryMap.putAll(child.collectEntries(childPrefix, flattenEntries)); } } return entryMap; } /** * @return the number of keys contained, directly or indirectly, in this PropertyTree */ public int size() { return entrySet().size(); } /** * @see java.util.Map#isEmpty() */ public boolean isEmpty() { return entrySet().isEmpty(); } /** * Returns an unmodifiable Collection containing the values of all of the entries of this PropertyTree. * * @see java.util.Map#values() */ public Collection values() { ArrayList values = new ArrayList(); Set entrySet = entrySet(); for (Iterator i = entrySet.iterator(); i.hasNext();) { Map.Entry e = (Map.Entry) i.next(); values.add(e.getValue()); } return Collections.unmodifiableList(values); } /** * Returns an unmodifiable Set containing the keys of all of the entries of this PropertyTree. * * @see java.util.Map#keySet() */ public Set keySet() { LinkedHashSet keys = new LinkedHashSet(); Set entrySet = entrySet(); for (Iterator i = entrySet.iterator(); i.hasNext();) { Map.Entry e = (Map.Entry) i.next(); keys.add(e.getKey()); } return Collections.unmodifiableSet(keys); } /** * @see java.util.Map#containsKey(java.lang.Object) */ public boolean containsKey(Object key) { validateKey(key); boolean containsKey = false; Set entrySet = entrySet(); for (Iterator i = entrySet.iterator(); !containsKey && i.hasNext();) { Map.Entry e = (Map.Entry) i.next(); Object entryKey = e.getKey(); containsKey = (entryKey != null) && entryKey.equals(key); } return containsKey; } /** * @see java.util.Map#containsValue(java.lang.Object) */ public boolean containsValue(Object value) { validateValue(value); boolean containsValue = false; Set entrySet = entrySet(); for (Iterator i = entrySet.iterator(); !containsValue && i.hasNext();) { Map.Entry e = (Map.Entry) i.next(); Object entryValue = e.getValue(); containsValue = (entryValue != null) && entryValue.equals(value); } return containsValue; } /** * Traverses the tree structure until it finds the PropertyTree pointed to by the given key, and returns that PropertyTree * instance. * <p> * Only returns PropertyTree instances; if you want the String value pointed to by a given key, you must call toString() on the * returned PropertyTree (after verifying that it isn't null, of course). * * @see java.util.Map#get(java.lang.Object) */ public Object get(Object key) { validateKey(key); return getSubtree((String) key); } // unsupported operations /** * Unsupported, since you can't change the contents of a PropertyTree once it has been initialized. */ public void clear() { throw new UnsupportedOperationException(); } /** * Unsupported, since you can't change the contents of a PropertyTree once it has been initialized. */ public void putAll(Map t) { throw new UnsupportedOperationException(); } /** * Unsupported, since you can't change the contents of a PropertyTree once it has been initialized. */ public Object remove(Object key) { throw new UnsupportedOperationException(); } /** * Unsupported, since you can't change the contents of a PropertyTree once it has been initialized. */ public Object put(Object key, Object value) { throw new UnsupportedOperationException(); } }