001/* 002 * Copyright 2005-2008 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.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/ecl2.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 */ 016package org.kuali.rice.kns.util.properties; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.Collections; 021import java.util.Iterator; 022import java.util.LinkedHashMap; 023import java.util.LinkedHashSet; 024import java.util.Map; 025import java.util.Properties; 026import java.util.Set; 027 028import org.apache.commons.lang.StringUtils; 029import 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 */ 043public 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 * Associates the given key with the given value. If the given key has multiple levels (consists of multiple strings separated 080 * by '.'), the property value is stored such that it can be retrieved either directly, by calling get() and passing the entire 081 * key; or indirectly, by decomposing the key into its separate levels and calling get() successively on the result of the 082 * previous level's get. <br> 083 * For example, given <br> 084 * <code> 085 * PropertyTree tree = new PropertyTree(); 086 * tree.set( "a.b.c", "something" ); 087 * </code> the following statements are 088 * equivalent ways to retrieve the value: <br> 089 * <code> 090 * Object one = tree.get( "a.b.c" ); 091 * </code> 092 * <code> 093 * Object two = tree.get( "a" ).get( "b" ).get( "c" ); 094 * </code><br> 095 * Note: since I can't have the get method return both a PropertyTree and a String, getting an actual String requires calling 096 * toString on the PropertyTree returned by get. 097 * 098 * @param key 099 * @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}