Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
JstlPropertyHolder |
|
| 2.311111111111111;2.311 | ||||
JstlPropertyHolder$PropertyTree |
|
| 2.311111111111111;2.311 |
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.krad.util; | |
17 | ||
18 | import org.apache.commons.lang.StringUtils; | |
19 | import org.apache.log4j.Logger; | |
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 | /** | |
32 | * This class implements the Map interface for a Properties instance. Exports all properties from the given Properties instance as | |
33 | * constants, usable from jstl. Implements the Map interface (by delegating everything to the PropertyTree, which really implements | |
34 | * the Map methods directly) so that jstl can translate ${Constants.a} into a call to ConfigConstants.get( "a" ). | |
35 | * <p> | |
36 | * The contents of this Map cannot be changed once it has been initialized. Any calls to any of the Map methods made before the | |
37 | * propertyTree has been initialized (i.e. before setProperties has been called) will throw an IllegalStateException. | |
38 | * <p> | |
39 | * Jstl converts ${Constants.a.b.c} into get("a").get("b").get("c"), so the properties are stored in a PropertyTree, which converts | |
40 | * the initial set( "a.b.c", "value" ) into construction of the necessary tree structure to support get("a").get("b").get("c"). | |
41 | * <p> | |
42 | * Implicitly relies on the assumption that the JSP will be calling toString() on the result of the final <code>get</code>, since | |
43 | * <code>get</code> can only return one type, and that type must be the complex one so that further dereferencing will be | |
44 | * possible. | |
45 | * | |
46 | * | |
47 | */ | |
48 | ||
49 | public class JstlPropertyHolder implements Map { | |
50 | private PropertyTree propertyTree; | |
51 | ||
52 | /** | |
53 | * Default constructor | |
54 | */ | |
55 | 0 | public JstlPropertyHolder() { |
56 | 0 | propertyTree = null; |
57 | 0 | } |
58 | ||
59 | /** | |
60 | * Creates a propertyTree to store the given properties | |
61 | * | |
62 | * @param properties | |
63 | */ | |
64 | public void setProperties(Properties properties) { | |
65 | 0 | propertyTree = new PropertyTree(properties); |
66 | 0 | } |
67 | ||
68 | ||
69 | /** | |
70 | * Copies in the given propertyTree rather than building its own. Reasonably dangerous, since that tree might presumably be | |
71 | * modified, violating the readonlyness of this datastructure. | |
72 | * | |
73 | * @param properties | |
74 | */ | |
75 | protected void setPropertyTree(PropertyTree tree) { | |
76 | 0 | propertyTree = tree; |
77 | 0 | } |
78 | ||
79 | ||
80 | // delegated methods | |
81 | @Override | |
82 | public Object get(Object key) { | |
83 | 0 | if (propertyTree == null) { |
84 | 0 | throw new IllegalStateException("propertyTree has not been initialized"); |
85 | } | |
86 | 0 | return this.propertyTree.get(key); |
87 | } | |
88 | ||
89 | @Override | |
90 | public int size() { | |
91 | 0 | if (propertyTree == null) { |
92 | 0 | throw new IllegalStateException("propertyTree has not been initialized"); |
93 | } | |
94 | 0 | return this.propertyTree.size(); |
95 | } | |
96 | ||
97 | @Override | |
98 | public void clear() { | |
99 | 0 | if (propertyTree == null) { |
100 | 0 | throw new IllegalStateException("propertyTree has not been initialized"); |
101 | } | |
102 | 0 | this.propertyTree.clear(); |
103 | 0 | } |
104 | ||
105 | @Override | |
106 | public boolean isEmpty() { | |
107 | 0 | if (propertyTree == null) { |
108 | 0 | throw new IllegalStateException("propertyTree has not been initialized"); |
109 | } | |
110 | 0 | return this.propertyTree.isEmpty(); |
111 | } | |
112 | ||
113 | @Override | |
114 | public boolean containsKey(Object key) { | |
115 | 0 | if (propertyTree == null) { |
116 | 0 | throw new IllegalStateException("propertyTree has not been initialized"); |
117 | } | |
118 | 0 | return this.propertyTree.containsKey(key); |
119 | } | |
120 | ||
121 | @Override | |
122 | public boolean containsValue(Object value) { | |
123 | 0 | if (propertyTree == null) { |
124 | 0 | throw new IllegalStateException("propertyTree has not been initialized"); |
125 | } | |
126 | 0 | return this.propertyTree.containsValue(value); |
127 | } | |
128 | ||
129 | @Override | |
130 | public Collection values() { | |
131 | 0 | if (propertyTree == null) { |
132 | 0 | throw new IllegalStateException("propertyTree has not been initialized"); |
133 | } | |
134 | 0 | return this.propertyTree.values(); |
135 | } | |
136 | ||
137 | @Override | |
138 | public void putAll(Map m) { | |
139 | 0 | if (propertyTree == null) { |
140 | 0 | throw new IllegalStateException("propertyTree has not been initialized"); |
141 | } | |
142 | 0 | this.propertyTree.putAll(m); |
143 | 0 | } |
144 | ||
145 | @Override | |
146 | public Set entrySet() { | |
147 | 0 | if (propertyTree == null) { |
148 | 0 | throw new IllegalStateException("propertyTree has not been initialized"); |
149 | } | |
150 | 0 | return this.propertyTree.entrySet(); |
151 | } | |
152 | ||
153 | @Override | |
154 | public Set keySet() { | |
155 | 0 | if (propertyTree == null) { |
156 | 0 | throw new IllegalStateException("propertyTree has not been initialized"); |
157 | } | |
158 | 0 | return this.propertyTree.keySet(); |
159 | } | |
160 | ||
161 | @Override | |
162 | public Object remove(Object key) { | |
163 | 0 | if (propertyTree == null) { |
164 | 0 | throw new IllegalStateException("propertyTree has not been initialized"); |
165 | } | |
166 | 0 | return this.propertyTree.remove(key); |
167 | } | |
168 | ||
169 | @Override | |
170 | public Object put(Object key, Object value) { | |
171 | 0 | if (propertyTree == null) { |
172 | 0 | throw new IllegalStateException("propertyTree has not been initialized"); |
173 | } | |
174 | 0 | return this.propertyTree.put(key, value); |
175 | } | |
176 | ||
177 | /** | |
178 | * This class is a Recursive container for single- and multi-level key,value pairs. It relies on the assumption that the consumer | |
179 | * (presumably a JSP) will (implicitly) call toString at the end of the chain, which will return the String value of the chain's | |
180 | * endpoint. | |
181 | * | |
182 | * It implements Map because that's how we fool jstl into converting "a.b.c" into get("a").get("b").get("c") instead of | |
183 | * getA().getB().getC() | |
184 | * | |
185 | * Uses LinkedHashMap and LinkedHashSet because iteration order is now important. | |
186 | * | |
187 | * | |
188 | */ | |
189 | static class PropertyTree implements Map { | |
190 | 1 | private static Logger LOG = Logger.getLogger(PropertyTree.class); |
191 | ||
192 | final boolean flat; | |
193 | final PropertyTree parent; | |
194 | String directValue; | |
195 | Map children; | |
196 | ||
197 | /** | |
198 | * Creates an empty instance with no parent | |
199 | */ | |
200 | public PropertyTree() { | |
201 | 79 | this(false); |
202 | 79 | } |
203 | ||
204 | /** | |
205 | * Creates an empty instance with no parent. If flat is true, entrySet and size and the iterators will ignore entries in | |
206 | * subtrees. | |
207 | */ | |
208 | 79 | public PropertyTree(boolean flat) { |
209 | 79 | this.parent = null; |
210 | 79 | this.children = new LinkedHashMap(); |
211 | 79 | this.flat = flat; |
212 | 79 | } |
213 | ||
214 | /** | |
215 | * Creates an empty instance with the given parent. If flat is true, entrySet and size and the iterators will ignore entries in | |
216 | * subtrees. | |
217 | */ | |
218 | 355 | private PropertyTree(PropertyTree parent) { |
219 | 355 | this.parent = parent; |
220 | 355 | this.children = new LinkedHashMap(); |
221 | 355 | this.flat = parent.flat; |
222 | 355 | } |
223 | ||
224 | /** | |
225 | * Creates an instance pre-loaded with the given Properties | |
226 | * | |
227 | * @param properties | |
228 | */ | |
229 | public PropertyTree(Properties properties) { | |
230 | 33 | this(); |
231 | ||
232 | 33 | setProperties(properties); |
233 | 33 | } |
234 | ||
235 | /** | |
236 | * Associates the given key with the given value. If the given key has multiple levels (consists of multiple strings separated | |
237 | * by '.'), the property value is stored such that it can be retrieved either directly, by calling get() and passing the entire | |
238 | * key; or indirectly, by decomposing the key into its separate levels and calling get() successively on the result of the | |
239 | * previous level's get. <br> | |
240 | * For example, given <br> | |
241 | * <code> | |
242 | * PropertyTree tree = new PropertyTree(); | |
243 | * tree.set( "a.b.c", "something" ); | |
244 | * </code> the following statements are | |
245 | * equivalent ways to retrieve the value: <br> | |
246 | * <code> | |
247 | * Object one = tree.get( "a.b.c" ); | |
248 | * </code> | |
249 | * <code> | |
250 | * Object two = tree.get( "a" ).get( "b" ).get( "c" ); | |
251 | * </code><br> | |
252 | * Note: since I can't have the get method return both a PropertyTree and a String, getting an actual String requires calling | |
253 | * toString on the PropertyTree returned by get. | |
254 | * | |
255 | * @param key | |
256 | * @param value | |
257 | * @throws IllegalArgumentException if the key is null | |
258 | * @throws IllegalArgumentException if the value is null | |
259 | */ | |
260 | public void setProperty(String key, String value) { | |
261 | 508 | validateKey(key); |
262 | 508 | validateValue(value); |
263 | ||
264 | 508 | if (parent == null) { |
265 | 237 | LOG.debug("setting (k,v) (" + key + "," + value + ")"); |
266 | } | |
267 | ||
268 | 508 | if (StringUtils.contains(key, '.')) { |
269 | 271 | String prefix = StringUtils.substringBefore(key, "."); |
270 | 271 | String suffix = StringUtils.substringAfter(key, "."); |
271 | ||
272 | 271 | PropertyTree node = getChild(prefix); |
273 | 271 | node.setProperty(suffix, value); |
274 | 271 | } |
275 | else { | |
276 | 237 | PropertyTree node = getChild(key); |
277 | 237 | node.setDirectValue(value); |
278 | } | |
279 | 508 | } |
280 | ||
281 | /** | |
282 | * Inserts all properties from the given Properties instance into this PropertyTree. | |
283 | * | |
284 | * @param properties | |
285 | * @throws IllegalArgumentException if the Properties object is null | |
286 | * @throws IllegalArgumentException if a property's key is null | |
287 | * @throws IllegalArgumentException if a property's value is null | |
288 | */ | |
289 | public void setProperties(Properties properties) { | |
290 | 33 | if (properties == null) { |
291 | 0 | throw new IllegalArgumentException("invalid (null) Properties object"); |
292 | } | |
293 | ||
294 | 33 | for (Iterator i = properties.entrySet().iterator(); i.hasNext();) { |
295 | 237 | Entry e = (Entry) i.next(); |
296 | 237 | setProperty((String) e.getKey(), (String) e.getValue()); |
297 | 237 | } |
298 | 33 | } |
299 | ||
300 | /** | |
301 | * Returns the PropertyTree object with the given key, or null if there is none. | |
302 | * | |
303 | * @param key | |
304 | * @return | |
305 | * @throws IllegalArgumentException if the key is null | |
306 | */ | |
307 | private PropertyTree getSubtree(String key) { | |
308 | 15 | validateKey(key); |
309 | ||
310 | 15 | PropertyTree returnValue = null; |
311 | 15 | if (StringUtils.contains(key, '.')) { |
312 | 4 | String prefix = StringUtils.substringBefore(key, "."); |
313 | 4 | String suffix = StringUtils.substringAfter(key, "."); |
314 | ||
315 | 4 | PropertyTree child = (PropertyTree) this.children.get(prefix); |
316 | 4 | if (child != null) { |
317 | 4 | returnValue = child.getSubtree(suffix); |
318 | } | |
319 | 4 | } |
320 | else { | |
321 | 11 | returnValue = (PropertyTree) this.children.get(key); |
322 | } | |
323 | ||
324 | 15 | return returnValue; |
325 | } | |
326 | ||
327 | ||
328 | /** | |
329 | * @param key | |
330 | * @return the directValue of the PropertyTree associated with the given key, or null if there is none | |
331 | */ | |
332 | public String getProperty(String key) { | |
333 | 0 | String propertyValue = null; |
334 | ||
335 | 0 | PropertyTree subtree = getSubtree(key); |
336 | 0 | if (subtree != null) { |
337 | 0 | propertyValue = subtree.getDirectValue(); |
338 | } | |
339 | ||
340 | 0 | return propertyValue; |
341 | } | |
342 | ||
343 | ||
344 | /** | |
345 | * @return an unmodifiable copy of the direct children of this PropertyTree | |
346 | */ | |
347 | public Map getDirectChildren() { | |
348 | 0 | return Collections.unmodifiableMap(this.children); |
349 | } | |
350 | ||
351 | ||
352 | /** | |
353 | * Returns the directValue of this PropertyTree, or null if there is none. | |
354 | * <p> | |
355 | * 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 | |
356 | * a complex (multi-part) key. | |
357 | */ | |
358 | public String toString() { | |
359 | 6 | return getDirectValue(); |
360 | } | |
361 | ||
362 | /** | |
363 | * Sets the directValue of this PropertyTree to the given value. | |
364 | * | |
365 | * @param value | |
366 | */ | |
367 | private void setDirectValue(String value) { | |
368 | 237 | validateValue(value); |
369 | ||
370 | 237 | this.directValue = value; |
371 | 237 | } |
372 | ||
373 | /** | |
374 | * @return directValue of this PropertyTree, or null if there is none | |
375 | */ | |
376 | private String getDirectValue() { | |
377 | 163 | return this.directValue; |
378 | } | |
379 | ||
380 | /** | |
381 | * @return true if the directValue of this PropertyTree is not null | |
382 | */ | |
383 | private boolean hasDirectValue() { | |
384 | 237 | return (this.directValue != null); |
385 | } | |
386 | ||
387 | /** | |
388 | * @return true if the this PropertyTree has children | |
389 | */ | |
390 | private boolean hasChildren() { | |
391 | 237 | return (!this.children.isEmpty()); |
392 | } | |
393 | ||
394 | /** | |
395 | * Returns the PropertyTree associated with the given key. If none exists, creates a new PropertyTree associates it with the | |
396 | * given key, and returns it. | |
397 | * | |
398 | * @param key | |
399 | * @return PropertyTree associated with the given key | |
400 | * @throws IllegalArgumentException if the given key is null | |
401 | */ | |
402 | private PropertyTree getChild(String key) { | |
403 | 508 | validateKey(key); |
404 | ||
405 | 508 | PropertyTree child = (PropertyTree) this.children.get(key); |
406 | 508 | if (child == null) { |
407 | 355 | child = new PropertyTree(this); |
408 | 355 | this.children.put(key, child); |
409 | } | |
410 | ||
411 | 508 | return child; |
412 | } | |
413 | ||
414 | /** | |
415 | * @param key | |
416 | * @throws IllegalArgumentException if the given key is not a String, or is null | |
417 | */ | |
418 | private void validateKey(Object key) { | |
419 | 1050 | if (!(key instanceof String)) { |
420 | 2 | throw new IllegalArgumentException("invalid (non-String) key"); |
421 | } | |
422 | 1048 | else if (key == null) { |
423 | 0 | throw new IllegalArgumentException("invalid (null) key"); |
424 | } | |
425 | 1048 | } |
426 | ||
427 | /** | |
428 | * @param value | |
429 | * @throws IllegalArgumentException if the given value is not a String, or is null | |
430 | */ | |
431 | private void validateValue(Object value) { | |
432 | 752 | if (!(value instanceof String)) { |
433 | 1 | throw new IllegalArgumentException("invalid (non-String) value"); |
434 | } | |
435 | 751 | else if (value == null) { |
436 | 0 | throw new IllegalArgumentException("invalid (null) value"); |
437 | } | |
438 | 751 | } |
439 | ||
440 | ||
441 | // Map methods | |
442 | /** | |
443 | * Returns an unmodifiable Set containing all key,value pairs in this PropertyTree and its children. | |
444 | * | |
445 | * @see java.util.Map#entrySet() | |
446 | */ | |
447 | public Set entrySet() { | |
448 | 35 | return Collections.unmodifiableSet(collectEntries(null, this.flat).entrySet()); |
449 | } | |
450 | ||
451 | /** | |
452 | * Builds a HashMap containing all of the key,value pairs stored in this PropertyTree | |
453 | * | |
454 | * @return | |
455 | */ | |
456 | private Map collectEntries(String prefix, boolean flattenEntries) { | |
457 | 137 | LinkedHashMap entryMap = new LinkedHashMap(); |
458 | ||
459 | 137 | for (Iterator i = this.children.entrySet().iterator(); i.hasNext();) { |
460 | 237 | Entry e = (Entry) i.next(); |
461 | 237 | PropertyTree child = (PropertyTree) e.getValue(); |
462 | 237 | String childKey = (String) e.getKey(); |
463 | ||
464 | // handle children with values | |
465 | 237 | if (child.hasDirectValue()) { |
466 | 157 | String entryKey = (prefix == null) ? childKey : prefix + "." + childKey; |
467 | 157 | String entryValue = child.getDirectValue(); |
468 | ||
469 | 157 | entryMap.put(entryKey, entryValue); |
470 | } | |
471 | ||
472 | // handle children with children | |
473 | 237 | if (!flattenEntries && child.hasChildren()) { |
474 | 102 | String childPrefix = (prefix == null) ? childKey : prefix + "." + childKey; |
475 | ||
476 | 102 | entryMap.putAll(child.collectEntries(childPrefix, flattenEntries)); |
477 | } | |
478 | 237 | } |
479 | ||
480 | 137 | return entryMap; |
481 | } | |
482 | ||
483 | /** | |
484 | * @return the number of keys contained, directly or indirectly, in this PropertyTree | |
485 | */ | |
486 | public int size() { | |
487 | 4 | return entrySet().size(); |
488 | } | |
489 | ||
490 | /** | |
491 | * @see java.util.Map#isEmpty() | |
492 | */ | |
493 | public boolean isEmpty() { | |
494 | 4 | return entrySet().isEmpty(); |
495 | } | |
496 | ||
497 | /** | |
498 | * Returns an unmodifiable Collection containing the values of all of the entries of this PropertyTree. | |
499 | * | |
500 | * @see java.util.Map#values() | |
501 | */ | |
502 | public Collection values() { | |
503 | 5 | ArrayList values = new ArrayList(); |
504 | ||
505 | 5 | Set entrySet = entrySet(); |
506 | 5 | for (Iterator i = entrySet.iterator(); i.hasNext();) { |
507 | 15 | Entry e = (Entry) i.next(); |
508 | ||
509 | 15 | values.add(e.getValue()); |
510 | 15 | } |
511 | ||
512 | 5 | return Collections.unmodifiableList(values); |
513 | } | |
514 | ||
515 | /** | |
516 | * Returns an unmodifiable Set containing the keys of all of the entries of this PropertyTree. | |
517 | * | |
518 | * @see java.util.Map#keySet() | |
519 | */ | |
520 | public Set keySet() { | |
521 | 5 | LinkedHashSet keys = new LinkedHashSet(); |
522 | ||
523 | 5 | Set entrySet = entrySet(); |
524 | 5 | for (Iterator i = entrySet.iterator(); i.hasNext();) { |
525 | 15 | Entry e = (Entry) i.next(); |
526 | ||
527 | 15 | keys.add(e.getKey()); |
528 | 15 | } |
529 | ||
530 | 5 | return Collections.unmodifiableSet(keys); |
531 | } | |
532 | ||
533 | /** | |
534 | * @see java.util.Map#containsKey(Object) | |
535 | */ | |
536 | public boolean containsKey(Object key) { | |
537 | 7 | validateKey(key); |
538 | ||
539 | 6 | boolean containsKey = false; |
540 | ||
541 | 6 | Set entrySet = entrySet(); |
542 | 6 | for (Iterator i = entrySet.iterator(); !containsKey && i.hasNext();) { |
543 | 26 | Entry e = (Entry) i.next(); |
544 | ||
545 | 26 | Object entryKey = e.getKey(); |
546 | 26 | containsKey = (entryKey != null) && entryKey.equals(key); |
547 | 26 | } |
548 | ||
549 | 6 | return containsKey; |
550 | } | |
551 | ||
552 | /** | |
553 | * @see java.util.Map#containsValue(Object) | |
554 | */ | |
555 | public boolean containsValue(Object value) { | |
556 | 7 | validateValue(value); |
557 | ||
558 | 6 | boolean containsValue = false; |
559 | ||
560 | 6 | Set entrySet = entrySet(); |
561 | 6 | for (Iterator i = entrySet.iterator(); !containsValue && i.hasNext();) { |
562 | 26 | Entry e = (Entry) i.next(); |
563 | ||
564 | 26 | Object entryValue = e.getValue(); |
565 | 26 | containsValue = (entryValue != null) && entryValue.equals(value); |
566 | 26 | } |
567 | ||
568 | 6 | return containsValue; |
569 | } | |
570 | ||
571 | /** | |
572 | * Traverses the tree structure until it finds the PropertyTree pointed to by the given key, and returns that PropertyTree | |
573 | * instance. | |
574 | * <p> | |
575 | * Only returns PropertyTree instances; if you want the String value pointed to by a given key, you must call toString() on the | |
576 | * returned PropertyTree (after verifying that it isn't null, of course). | |
577 | * | |
578 | * @see java.util.Map#get(Object) | |
579 | */ | |
580 | public Object get(Object key) { | |
581 | 12 | validateKey(key); |
582 | ||
583 | 11 | return getSubtree((String) key); |
584 | } | |
585 | ||
586 | ||
587 | // unsupported operations | |
588 | /** | |
589 | * Unsupported, since you can't change the contents of a PropertyTree once it has been initialized. | |
590 | */ | |
591 | public void clear() { | |
592 | 1 | throw new UnsupportedOperationException(); |
593 | } | |
594 | ||
595 | /** | |
596 | * Unsupported, since you can't change the contents of a PropertyTree once it has been initialized. | |
597 | */ | |
598 | public void putAll(Map t) { | |
599 | 1 | throw new UnsupportedOperationException(); |
600 | } | |
601 | ||
602 | /** | |
603 | * Unsupported, since you can't change the contents of a PropertyTree once it has been initialized. | |
604 | */ | |
605 | public Object remove(Object key) { | |
606 | 1 | throw new UnsupportedOperationException(); |
607 | } | |
608 | ||
609 | /** | |
610 | * Unsupported, since you can't change the contents of a PropertyTree once it has been initialized. | |
611 | */ | |
612 | public Object put(Object key, Object value) { | |
613 | 1 | throw new UnsupportedOperationException(); |
614 | } | |
615 | } | |
616 | } |