001/* 002 * The Kuali Financial System, a comprehensive financial management system for higher education. 003 * 004 * Copyright 2005-2014 The Kuali Foundation 005 * 006 * This program is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU Affero General Public License as 008 * published by the Free Software Foundation, either version 3 of the 009 * License, or (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU Affero General Public License for more details. 015 * 016 * You should have received a copy of the GNU Affero General Public License 017 * along with this program. If not, see <http://www.gnu.org/licenses/>. 018 */ 019package org.kuali.rice.kns.util.properties; 020 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.Iterator; 025import java.util.LinkedHashMap; 026import java.util.LinkedHashSet; 027import java.util.Map; 028import java.util.Properties; 029import java.util.Set; 030 031import org.apache.commons.lang.StringUtils; 032import org.apache.log4j.Logger; 033 034/** 035 * This class is a Recursive container for single- and multi-level key,value pairs. It relies on the assumption that the consumer 036 * (presumably a JSP) will (implicitly) call toString at the end of the chain, which will return the String value of the chain's 037 * endpoint. 038 * 039 * It implements Map because that's how we fool jstl into converting "a.b.c" into get("a").get("b").get("c") instead of 040 * getA().getB().getC() 041 * 042 * Uses LinkedHashMap and LinkedHashSet because iteration order is now important. 043 * 044 * 045 */ 046public class PropertyTree implements Map { 047 private static Logger LOG = Logger.getLogger(PropertyTree.class); 048 049 final boolean flat; 050 final PropertyTree parent; 051 String directValue; 052 Map children; 053 054 /** 055 * Creates an empty instance with no parent 056 */ 057 public PropertyTree() { 058 this(false); 059 } 060 061 /** 062 * Creates an empty instance with no parent. If flat is true, entrySet and size and the iterators will ignore entries in 063 * subtrees. 064 */ 065 public PropertyTree(boolean flat) { 066 this.parent = null; 067 this.children = new LinkedHashMap(); 068 this.flat = flat; 069 } 070 071 /** 072 * Creates an empty instance with the given parent. If flat is true, entrySet and size and the iterators will ignore entries in 073 * subtrees. 074 */ 075 private PropertyTree(PropertyTree parent) { 076 this.parent = parent; 077 this.children = new LinkedHashMap(); 078 this.flat = parent.flat; 079 } 080 081 /** 082 * Associates the given key with the given value. If the given key has multiple levels (consists of multiple strings separated 083 * by '.'), the property value is stored such that it can be retrieved either directly, by calling get() and passing the entire 084 * key; or indirectly, by decomposing the key into its separate levels and calling get() successively on the result of the 085 * previous level's get. <br> 086 * For example, given <br> 087 * <code> 088 * PropertyTree tree = new PropertyTree(); 089 * tree.set( "a.b.c", "something" ); 090 * </code> the following statements are 091 * equivalent ways to retrieve the value: <br> 092 * <code> 093 * Object one = tree.get( "a.b.c" ); 094 * </code> 095 * <code> 096 * Object two = tree.get( "a" ).get( "b" ).get( "c" ); 097 * </code><br> 098 * Note: since I can't have the get method return both a PropertyTree and a String, getting an actual String requires calling 099 * 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}