001: /*
002: * Copyright 2005-2006 The Kuali Foundation.
003: *
004: * Licensed under the Educational Community License, Version 1.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/ecl1.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: */
016: package org.kuali.core.util.properties;
017:
018: import java.util.ArrayList;
019: import java.util.Collection;
020: import java.util.Collections;
021: import java.util.Iterator;
022: import java.util.LinkedHashMap;
023: import java.util.LinkedHashSet;
024: import java.util.Map;
025: import java.util.Properties;
026: import java.util.Set;
027:
028: import org.apache.commons.lang.StringUtils;
029: import 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: */
043: public 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: * Creates an instance pre-loaded with the given Properties
080: *
081: * @param properties
082: */
083: public PropertyTree(Properties properties) {
084: this ();
085:
086: setProperties(properties);
087: }
088:
089: /**
090: * Associates the given key with the given value. If the given key has multiple levels (consists of multiple strings separated
091: * by '.'), the property value is stored such that it can be retrieved either directly, by calling get() and passing the entire
092: * key; or indirectly, by decomposing the key into its separate levels and calling get() successively on the result of the
093: * previous level's get. <br>
094: * For example, given <br>
095: * <code>
096: * PropertyTree tree = new PropertyTree();
097: * tree.set( "a.b.c", "something" );
098: * </code> the following statements are
099: * equivalent ways to retrieve the value: <br>
100: * <code>
101: * Object one = tree.get( "a.b.c" );
102: * </code>
103: * <code>
104: * Object two = tree.get( "a" ).get( "b" ).get( "c" );
105: * </code><br>
106: * Note: since I can't have the get method return both a PropertyTree and a String, getting an actual String requires calling
107: * toString on the PropertyTree returned by get.
108: *
109: * @param key
110: * @param value
111: * @throws IllegalArgumentException if the key is null
112: * @throws IllegalArgumentException if the value is null
113: */
114: public void setProperty(String key, String value) {
115: validateKey(key);
116: validateValue(value);
117:
118: if (parent == null) {
119: LOG.debug("setting (k,v) (" + key + "," + value + ")");
120: }
121:
122: if (StringUtils.contains(key, '.')) {
123: String prefix = StringUtils.substringBefore(key, ".");
124: String suffix = StringUtils.substringAfter(key, ".");
125:
126: PropertyTree node = getChild(prefix);
127: node.setProperty(suffix, value);
128: } else {
129: PropertyTree node = getChild(key);
130: node.setDirectValue(value);
131: }
132: }
133:
134: /**
135: * Inserts all properties from the given Properties instance into this PropertyTree.
136: *
137: * @param properties
138: * @throws IllegalArgumentException if the Properties object is null
139: * @throws IllegalArgumentException if a property's key is null
140: * @throws IllegalArgumentException if a property's value is null
141: */
142: public void setProperties(Properties properties) {
143: if (properties == null) {
144: throw new IllegalArgumentException(
145: "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
170: .get(prefix);
171: if (child != null) {
172: returnValue = child.getSubtree(suffix);
173: }
174: } else {
175: returnValue = (PropertyTree) this .children.get(key);
176: }
177:
178: return returnValue;
179: }
180:
181: /**
182: * @param key
183: * @return the directValue of the PropertyTree associated with the given key, or null if there is none
184: */
185: public String getProperty(String key) {
186: String propertyValue = null;
187:
188: PropertyTree subtree = getSubtree(key);
189: if (subtree != null) {
190: propertyValue = subtree.getDirectValue();
191: }
192:
193: return propertyValue;
194: }
195:
196: /**
197: * @return an unmodifiable copy of the direct children of this PropertyTree
198: */
199: public Map getDirectChildren() {
200: return Collections.unmodifiableMap(this .children);
201: }
202:
203: /**
204: * Returns the directValue of this PropertyTree, or null if there is none.
205: * <p>
206: * 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
207: * a complex (multi-part) key.
208: */
209: public String toString() {
210: return getDirectValue();
211: }
212:
213: /**
214: * Sets the directValue of this PropertyTree to the given value.
215: *
216: * @param value
217: */
218: private void setDirectValue(String value) {
219: validateValue(value);
220:
221: this .directValue = value;
222: }
223:
224: /**
225: * @return directValue of this PropertyTree, or null if there is none
226: */
227: private String getDirectValue() {
228: return this .directValue;
229: }
230:
231: /**
232: * @return true if the directValue of this PropertyTree is not null
233: */
234: private boolean hasDirectValue() {
235: return (this .directValue != null);
236: }
237:
238: /**
239: * @return true if the this PropertyTree has children
240: */
241: private boolean hasChildren() {
242: return (!this .children.isEmpty());
243: }
244:
245: /**
246: * Returns the PropertyTree associated with the given key. If none exists, creates a new PropertyTree associates it with the
247: * given key, and returns it.
248: *
249: * @param key
250: * @return PropertyTree associated with the given key
251: * @throws IllegalArgumentException if the given key is null
252: */
253: private PropertyTree getChild(String key) {
254: validateKey(key);
255:
256: PropertyTree child = (PropertyTree) this .children.get(key);
257: if (child == null) {
258: child = new PropertyTree(this );
259: this .children.put(key, child);
260: }
261:
262: return child;
263: }
264:
265: /**
266: * @param key
267: * @throws IllegalArgumentException if the given key is not a String, or is null
268: */
269: private void validateKey(Object key) {
270: if (!(key instanceof String)) {
271: throw new IllegalArgumentException(
272: "invalid (non-String) key");
273: } else if (key == null) {
274: throw new IllegalArgumentException("invalid (null) key");
275: }
276: }
277:
278: /**
279: * @param value
280: * @throws IllegalArgumentException if the given value is not a String, or is null
281: */
282: private void validateValue(Object value) {
283: if (!(value instanceof String)) {
284: throw new IllegalArgumentException(
285: "invalid (non-String) value");
286: } else if (value == null) {
287: throw new IllegalArgumentException("invalid (null) value");
288: }
289: }
290:
291: // Map methods
292: /**
293: * Returns an unmodifiable Set containing all key,value pairs in this PropertyTree and its children.
294: *
295: * @see java.util.Map#entrySet()
296: */
297: public Set entrySet() {
298: return Collections.unmodifiableSet(collectEntries(null,
299: this .flat).entrySet());
300: }
301:
302: /**
303: * Builds a HashMap containing all of the key,value pairs stored in this PropertyTree
304: *
305: * @return
306: */
307: private Map collectEntries(String prefix, boolean flattenEntries) {
308: LinkedHashMap entryMap = new LinkedHashMap();
309:
310: for (Iterator i = this .children.entrySet().iterator(); i
311: .hasNext();) {
312: Map.Entry e = (Map.Entry) i.next();
313: PropertyTree child = (PropertyTree) e.getValue();
314: String childKey = (String) e.getKey();
315:
316: // handle children with values
317: if (child.hasDirectValue()) {
318: String entryKey = (prefix == null) ? childKey : prefix
319: + "." + childKey;
320: String entryValue = child.getDirectValue();
321:
322: entryMap.put(entryKey, entryValue);
323: }
324:
325: // handle children with children
326: if (!flattenEntries && child.hasChildren()) {
327: String childPrefix = (prefix == null) ? childKey
328: : prefix + "." + childKey;
329:
330: entryMap.putAll(child.collectEntries(childPrefix,
331: flattenEntries));
332: }
333: }
334:
335: return entryMap;
336: }
337:
338: /**
339: * @return the number of keys contained, directly or indirectly, in this PropertyTree
340: */
341: public int size() {
342: return entrySet().size();
343: }
344:
345: /**
346: * @see java.util.Map#isEmpty()
347: */
348: public boolean isEmpty() {
349: return entrySet().isEmpty();
350: }
351:
352: /**
353: * Returns an unmodifiable Collection containing the values of all of the entries of this PropertyTree.
354: *
355: * @see java.util.Map#values()
356: */
357: public Collection values() {
358: ArrayList values = new ArrayList();
359:
360: Set entrySet = entrySet();
361: for (Iterator i = entrySet.iterator(); i.hasNext();) {
362: Map.Entry e = (Map.Entry) i.next();
363:
364: values.add(e.getValue());
365: }
366:
367: return Collections.unmodifiableList(values);
368: }
369:
370: /**
371: * Returns an unmodifiable Set containing the keys of all of the entries of this PropertyTree.
372: *
373: * @see java.util.Map#keySet()
374: */
375: public Set keySet() {
376: LinkedHashSet keys = new LinkedHashSet();
377:
378: Set entrySet = entrySet();
379: for (Iterator i = entrySet.iterator(); i.hasNext();) {
380: Map.Entry e = (Map.Entry) i.next();
381:
382: keys.add(e.getKey());
383: }
384:
385: return Collections.unmodifiableSet(keys);
386: }
387:
388: /**
389: * @see java.util.Map#containsKey(java.lang.Object)
390: */
391: public boolean containsKey(Object key) {
392: validateKey(key);
393:
394: boolean containsKey = false;
395:
396: Set entrySet = entrySet();
397: for (Iterator i = entrySet.iterator(); !containsKey
398: && i.hasNext();) {
399: Map.Entry e = (Map.Entry) i.next();
400:
401: Object entryKey = e.getKey();
402: containsKey = (entryKey != null) && entryKey.equals(key);
403: }
404:
405: return containsKey;
406: }
407:
408: /**
409: * @see java.util.Map#containsValue(java.lang.Object)
410: */
411: public boolean containsValue(Object value) {
412: validateValue(value);
413:
414: boolean containsValue = false;
415:
416: Set entrySet = entrySet();
417: for (Iterator i = entrySet.iterator(); !containsValue
418: && i.hasNext();) {
419: Map.Entry e = (Map.Entry) i.next();
420:
421: Object entryValue = e.getValue();
422: containsValue = (entryValue != null)
423: && entryValue.equals(value);
424: }
425:
426: return containsValue;
427: }
428:
429: /**
430: * Traverses the tree structure until it finds the PropertyTree pointed to by the given key, and returns that PropertyTree
431: * instance.
432: * <p>
433: * Only returns PropertyTree instances; if you want the String value pointed to by a given key, you must call toString() on the
434: * returned PropertyTree (after verifying that it isn't null, of course).
435: *
436: * @see java.util.Map#get(java.lang.Object)
437: */
438: public Object get(Object key) {
439: validateKey(key);
440:
441: return getSubtree((String) key);
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: }
|