1 /*
2 * The Kuali Financial System, a comprehensive financial management system for higher education.
3 *
4 * Copyright 2005-2014 The Kuali Foundation
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, either version 3 of the
9 * License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Affero General Public License for more details.
15 *
16 * You should have received a copy of the GNU Affero General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19 package org.kuali.rice.kns.util.properties;
20
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.Iterator;
25 import java.util.LinkedHashMap;
26 import java.util.LinkedHashSet;
27 import java.util.Map;
28 import java.util.Properties;
29 import java.util.Set;
30
31 import org.apache.commons.lang.StringUtils;
32 import org.apache.log4j.Logger;
33
34 /**
35 * This class is a Recursive container for single- and multi-level key,value pairs. It relies on the assumption that the consumer
36 * (presumably a JSP) will (implicitly) call toString at the end of the chain, which will return the String value of the chain's
37 * endpoint.
38 *
39 * It implements Map because that's how we fool jstl into converting "a.b.c" into get("a").get("b").get("c") instead of
40 * getA().getB().getC()
41 *
42 * Uses LinkedHashMap and LinkedHashSet because iteration order is now important.
43 *
44 *
45 */
46 public class PropertyTree implements Map {
47 private static Logger LOG = Logger.getLogger(PropertyTree.class);
48
49 final boolean flat;
50 final PropertyTree parent;
51 String directValue;
52 Map children;
53
54 /**
55 * Creates an empty instance with no parent
56 */
57 public PropertyTree() {
58 this(false);
59 }
60
61 /**
62 * Creates an empty instance with no parent. If flat is true, entrySet and size and the iterators will ignore entries in
63 * subtrees.
64 */
65 public PropertyTree(boolean flat) {
66 this.parent = null;
67 this.children = new LinkedHashMap();
68 this.flat = flat;
69 }
70
71 /**
72 * Creates an empty instance with the given parent. If flat is true, entrySet and size and the iterators will ignore entries in
73 * subtrees.
74 */
75 private PropertyTree(PropertyTree parent) {
76 this.parent = parent;
77 this.children = new LinkedHashMap();
78 this.flat = parent.flat;
79 }
80
81 /**
82 * Associates the given key with the given value. If the given key has multiple levels (consists of multiple strings separated
83 * by '.'), the property value is stored such that it can be retrieved either directly, by calling get() and passing the entire
84 * key; or indirectly, by decomposing the key into its separate levels and calling get() successively on the result of the
85 * previous level's get. <br>
86 * For example, given <br>
87 * <code>
88 * PropertyTree tree = new PropertyTree();
89 * tree.set( "a.b.c", "something" );
90 * </code> the following statements are
91 * equivalent ways to retrieve the value: <br>
92 * <code>
93 * Object one = tree.get( "a.b.c" );
94 * </code>
95 * <code>
96 * Object two = tree.get( "a" ).get( "b" ).get( "c" );
97 * </code><br>
98 * Note: since I can't have the get method return both a PropertyTree and a String, getting an actual String requires calling
99 * 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 }