/** * Copyright 2005-2015 The Kuali Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ecl2.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.kuali.rice.core.api.util.collect; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; 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; /** * This class implements the Map interface for a Properties instance. Exports all properties from the given Properties instance as * constants, usable from jstl. Implements the Map interface (by delegating everything to the PropertyTree, which really implements * the Map methods directly) so that jstl can translate ${Constants.a} into a call to ConfigConstants.get( "a" ). * <p> * The contents of this Map cannot be changed once it has been initialized. Any calls to any of the Map methods made before the * propertyTree has been initialized (i.e. before setProperties has been called) will throw an IllegalStateException. * <p> * Jstl converts ${Constants.a.b.c} into get("a").get("b").get("c"), so the properties are stored in a PropertyTree, which converts * the initial set( "a.b.c", "value" ) into construction of the necessary tree structure to support get("a").get("b").get("c"). * <p> * Implicitly relies on the assumption that the JSP will be calling toString() on the result of the final <code>get</code>, since * <code>get</code> can only return one type, and that type must be the complex one so that further dereferencing will be * possible. */ //FIXME: use generics, make class threadsafe public final class PropertiesMap implements Map { private PropertyTree propertyTree; /** * Creates a propertyTree to store the given properties * * @param properties */ public void setProperties(Properties properties) { propertyTree = new PropertyTree(properties); } // delegated methods @Override public Object get(Object key) { if (propertyTree == null) { throw new IllegalStateException("propertyTree has not been initialized"); } return this.propertyTree.get(key); } @Override public int size() { if (propertyTree == null) { throw new IllegalStateException("propertyTree has not been initialized"); } return this.propertyTree.size(); } @Override public void clear() { if (propertyTree == null) { throw new IllegalStateException("propertyTree has not been initialized"); } this.propertyTree.clear(); } @Override public boolean isEmpty() { if (propertyTree == null) { throw new IllegalStateException("propertyTree has not been initialized"); } return this.propertyTree.isEmpty(); } @Override public boolean containsKey(Object key) { if (propertyTree == null) { throw new IllegalStateException("propertyTree has not been initialized"); } return this.propertyTree.containsKey(key); } @Override public boolean containsValue(Object value) { if (propertyTree == null) { throw new IllegalStateException("propertyTree has not been initialized"); } return this.propertyTree.containsValue(value); } @Override public Collection values() { if (propertyTree == null) { throw new IllegalStateException("propertyTree has not been initialized"); } return this.propertyTree.values(); } @Override public void putAll(Map m) { if (propertyTree == null) { throw new IllegalStateException("propertyTree has not been initialized"); } this.propertyTree.putAll(m); } @Override public Set entrySet() { if (propertyTree == null) { throw new IllegalStateException("propertyTree has not been initialized"); } return this.propertyTree.entrySet(); } @Override public Set keySet() { if (propertyTree == null) { throw new IllegalStateException("propertyTree has not been initialized"); } return this.propertyTree.keySet(); } @Override public Object remove(Object key) { if (propertyTree == null) { throw new IllegalStateException("propertyTree has not been initialized"); } return this.propertyTree.remove(key); } @Override public Object put(Object key, Object value) { if (propertyTree == null) { throw new IllegalStateException("propertyTree has not been initialized"); } return this.propertyTree.put(key, value); } /** * 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. * * */ static class PropertyTree implements Map { private static final 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; } /** * Creates an instance pre-loaded with the given Properties * * @param properties */ public PropertyTree(Properties properties) { this(); setProperties(properties); } /** * 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();) { Entry e = (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(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();) { Entry e = (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();) { Entry e = (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();) { Entry e = (Entry) i.next(); keys.add(e.getKey()); } return Collections.unmodifiableSet(keys); } /** * @see java.util.Map#containsKey(Object) */ public boolean containsKey(Object key) { validateKey(key); boolean containsKey = false; Set entrySet = entrySet(); for (Iterator i = entrySet.iterator(); !containsKey && i.hasNext();) { Entry e = (Entry) i.next(); Object entryKey = e.getKey(); containsKey = (entryKey != null) && entryKey.equals(key); } return containsKey; } /** * @see java.util.Map#containsValue(Object) */ public boolean containsValue(Object value) { validateValue(value); boolean containsValue = false; Set entrySet = entrySet(); for (Iterator i = entrySet.iterator(); !containsValue && i.hasNext();) { Entry e = (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(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(); } } }