1 /*
2 * Copyright 2005-2008 The Kuali Foundation
3 *
4 * Licensed under the Educational Community License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.opensource.org/licenses/ecl2.php
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.kuali.rice.kns.util.properties;
17
18 import java.util.ArrayList;
19 import java.util.Collection;
20 import java.util.Collections;
21 import java.util.Iterator;
22 import java.util.LinkedHashMap;
23 import java.util.LinkedHashSet;
24 import java.util.Map;
25 import java.util.Properties;
26 import java.util.Set;
27
28 import org.apache.commons.lang.StringUtils;
29 import org.apache.log4j.Logger;
30
31 /**
32 * This class is a Recursive container for single- and multi-level key,value pairs. It relies on the assumption that the consumer
33 * (presumably a JSP) will (implicitly) call toString at the end of the chain, which will return the String value of the chain's
34 * endpoint.
35 *
36 * It implements Map because that's how we fool jstl into converting "a.b.c" into get("a").get("b").get("c") instead of
37 * getA().getB().getC()
38 *
39 * Uses LinkedHashMap and LinkedHashSet because iteration order is now important.
40 *
41 *
42 */
43 public class PropertyTree implements Map {
44 private static Logger LOG = Logger.getLogger(PropertyTree.class);
45
46 final boolean flat;
47 final PropertyTree parent;
48 String directValue;
49 Map children;
50
51 /**
52 * Creates an empty instance with no parent
53 */
54 public PropertyTree() {
55 this(false);
56 }
57
58 /**
59 * Creates an empty instance with no parent. If flat is true, entrySet and size and the iterators will ignore entries in
60 * subtrees.
61 */
62 public PropertyTree(boolean flat) {
63 this.parent = null;
64 this.children = new LinkedHashMap();
65 this.flat = flat;
66 }
67
68 /**
69 * Creates an empty instance with the given parent. If flat is true, entrySet and size and the iterators will ignore entries in
70 * subtrees.
71 */
72 private PropertyTree(PropertyTree parent) {
73 this.parent = parent;
74 this.children = new LinkedHashMap();
75 this.flat = parent.flat;
76 }
77
78 /**
79 * Associates the given key with the given value. If the given key has multiple levels (consists of multiple strings separated
80 * by '.'), the property value is stored such that it can be retrieved either directly, by calling get() and passing the entire
81 * key; or indirectly, by decomposing the key into its separate levels and calling get() successively on the result of the
82 * previous level's get. <br>
83 * For example, given <br>
84 * <code>
85 * PropertyTree tree = new PropertyTree();
86 * tree.set( "a.b.c", "something" );
87 * </code> the following statements are
88 * equivalent ways to retrieve the value: <br>
89 * <code>
90 * Object one = tree.get( "a.b.c" );
91 * </code>
92 * <code>
93 * Object two = tree.get( "a" ).get( "b" ).get( "c" );
94 * </code><br>
95 * Note: since I can't have the get method return both a PropertyTree and a String, getting an actual String requires calling
96 * toString on the PropertyTree returned by get.
97 *
98 * @param key
99 * @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 }