001 package edu.nrao.sss.util; 002 003 import java.io.InputStream; 004 import java.lang.reflect.Method; 005 import java.util.ArrayList; 006 import java.util.EnumSet; 007 import java.util.HashMap; 008 import java.util.List; 009 import java.util.Map; 010 import java.util.Properties; 011 012 import org.apache.log4j.Logger; 013 014 015 /** 016 * A class that helps clients interact with enumeration classes. 017 * <p> 018 * Instances of this class will be used mainly, though not exclusively, by 019 * user interface classes. This class helps with setting default values for 020 * enumerations and also with restricting the choices of enumeration values 021 * available to users.</p> 022 * <p> 023 * There are two ways to use this class to help manage enumerations, and these 024 * two ways can be used together. The first is programmatic. The 025 * {@link #setDefaultValue(Class, Enum)} method can be used to define a default 026 * enumeration element for an enumeration class. The 027 * {@link #addToSelectableSet(Class, Enum)}, 028 * {@link #removeFromSelectableSet(Class, Enum)}, and 029 * {@link #setSelectableSet(Class, EnumSet)} methods can be used to control 030 * the elements of an enumeration class that may be chosen by a user 031 * (or another object).</p> 032 * <p> 033 * The second way to manage enumerations with this class is to use properties 034 * files. The name of each properties file is 035 * <tt><i>simple-enumeration-class-name</i>.properties</tt>. These files must 036 * reside on the classpath. This class understands these two properties: 037 * <tt><b>default</b></tt> and <tt><b>nonselectable</b></tt>. The first 038 * property takes a single value; the second takes a comma-separated list of 039 * values. The text of the values must be identical to the name of the 040 * element, including capitalization.</p> 041 * <p> 042 * <b><u>Example Properties File:</u></b><br/> 043 * Name: <tt>DistanceUnits.properties</tt><br/> 044 * Contents:<pre> 045 * 046 * default = KILOMETER 047 * 048 * nonselectable = UNKNOWN, ANGSTROM, NANOMETER, MICROMETER, MILLIMETER,\ 049 * CENTIMETER, MILE 050 * </pre> 051 * <p> 052 * <b>CVS Info:</b> 053 * <table style="margin-left:2em"> 054 * <tr><td>$Revision: 1707 $</td></tr> 055 * <tr><td>$Date: 2008-11-14 10:23:59 -0700 (Fri, 14 Nov 2008) $</td></tr> 056 * <tr><td>$Author: dharland $</td></tr> 057 * </table></p> 058 * 059 * @author David M. Harland 060 * @since 2006-05-18 061 */ 062 public class EnumerationUtility 063 { 064 private static Logger log = Logger.getLogger(EnumerationUtility.class); 065 066 //============================================================================ 067 // STATIC METHODS & INSTANCES 068 //============================================================================ 069 070 //An instance that could be shared by multiple clients. 071 private static EnumerationUtility sharedInst; 072 073 /** 074 * Returns a pre-made instance of this utility. 075 * Every request to this method returns the same object. 076 * 077 * @return a pre-made instance of this utility. 078 */ 079 public static EnumerationUtility getSharedInstance() 080 { 081 if (sharedInst == null) 082 sharedInst = new EnumerationUtility(); 083 084 return sharedInst; 085 } 086 087 //============================================================================ 088 // INSTANCE VARIABLES & CONSTRUCTORS 089 //============================================================================ 090 091 //Holds information about various enumeration classes. 092 @SuppressWarnings("unchecked") 093 private Map<Class, Value> enums; 094 095 /** Creates a new enumeration utility. */ 096 @SuppressWarnings("unchecked") 097 public EnumerationUtility() 098 { 099 enums = new HashMap<Class, Value>(); 100 } 101 102 //============================================================================ 103 // INSTANCE METHODS 104 //============================================================================ 105 106 /** 107 * Returns the complete set of enumeration elements for the given class. 108 * @param enumType an enumeration class. 109 * @return the complete set of enumeration elements for {@code enumType}. 110 * @throws NullPointerException if {@code enumType} is <i>null</i>. 111 */ 112 public <E extends Enum<E>> EnumSet<E> getCompleteSetFor(Class<E> enumType) 113 { 114 return getOrMakeMapValueFor(enumType).complete.clone(); 115 } 116 117 /** 118 * Returns the set of enumeration elements for the given class from which 119 * a specific element may be selected. This set is often, but not always, 120 * the complete set of elements. Sometimes, though, an enumeration has 121 * an element such as <tt>UNKNOWN</tt> that should not be a client-selectable 122 * element. The set returned by this method will typically exclude such 123 * elements. 124 * @param enumType an enumeration class. 125 * @return a set of selectable elements of type <tt>E</tt>. 126 * @throws NullPointerException if {@code enumType} is <i>null</i>. 127 */ 128 public <E extends Enum<E>> EnumSet<E> getSelectableSetFor(Class<E> enumType) 129 { 130 return getOrMakeMapValueFor(enumType).selectable.clone(); 131 } 132 133 /** 134 * Returns a default element for the given enumeration type. 135 * A common use of this method is to initialize a user interface 136 * component to a default value. 137 * @param enumType an enumeration class. 138 * @return a default element for {@code enumType}. 139 * @throws NullPointerException if {@code enumType} is <i>null</i>. 140 */ 141 public <E extends Enum<E>> E getDefaultValueFor(Class<E> enumType) 142 { 143 return getOrMakeMapValueFor(enumType).dfault; 144 } 145 146 /** 147 * Returns a random element for the given enumeration type. 148 * @param enumType an enumeration class. 149 * @return a random element for the given enumeration type. 150 */ 151 public <E extends Enum<E>> E getRandomValueFor(Class<E> enumType) 152 { 153 List<E> values = new ArrayList<E>(EnumSet.allOf(enumType)); 154 155 return values.get((int)(values.size() * Math.random())); 156 } 157 158 /** 159 * Sets the collection of selectable elements for the given enumeration type. 160 * @param enumType an enumeration class. 161 * @param selectableSet a set of selectable elements of type <tt>E</tt>. 162 * @throws NullPointerException if either parameter is <i>null</i>. 163 */ 164 public <E extends Enum<E>> void setSelectableSet(Class<E> enumType, 165 EnumSet<E> selectableSet) 166 { 167 getOrMakeMapValueFor(enumType).selectable = selectableSet.clone(); 168 } 169 170 /** 171 * Add {@code enumValue} to the set of selectable elements of class 172 * {@code enumType}. If {@code enumType} is <i>null</i>, this method 173 * does nothing. 174 * @param enumType an enumeration class. 175 * @param enumValue a selectable element. 176 * @throws NullPointerException if {@code enumType} is <i>null</i>. 177 */ 178 public <E extends Enum<E>> void addToSelectableSet(Class<E> enumType, E enumValue) 179 { 180 if (enumValue != null) 181 getOrMakeMapValueFor(enumType).selectable.add(enumValue); 182 } 183 184 /** 185 * Removes {@code enumValue} from the set of selectable elements of class 186 * {@code enumType}. 187 * @param enumType an enumeration class. 188 * @param enumValue an element that is not selectable. 189 * @throws NullPointerException if {@code enumType} is <i>null</i>. 190 */ 191 public <E extends Enum<E>> void removeFromSelectableSet(Class<E> enumType, E enumValue) 192 { 193 if (enumValue != null) 194 getOrMakeMapValueFor(enumType).selectable.remove(enumValue); 195 } 196 197 /** 198 * Sets the default value for {@code enumType} to {@code enumValue}. 199 * @param enumType an enumeration class. 200 * @param enumValue a default value for {@code enumType}. 201 */ 202 public <E extends Enum<E>> void setDefaultValue(Class<E> enumType, E enumValue) 203 { 204 if (enumValue != null) 205 getOrMakeMapValueFor(enumType).dfault = enumValue; 206 } 207 208 /** 209 * Reloads the internally stored information for the given enumeration 210 * type. This is useful if the client knows that a change has been 211 * made to an external properties file. Unless forced to do so, this 212 * utility will not detect such external changes. 213 * 214 * @param enumType an enumeration class. 215 */ 216 public <E extends Enum<E>> void reload(Class<E> enumType) 217 { 218 enums.put(enumType, new Value<E>(enumType)); 219 } 220 221 /** 222 * Reloads all internally stored information. 223 * See {@link #reload(Class)} for details. 224 */ 225 @SuppressWarnings("unchecked") 226 public void reload() 227 { 228 //The complier can't know what we know here: That the map is coordinated 229 //for Class<E> and Value<E>. It sees only Class & Value. 230 //We suppress this warning. 231 for (Class enumType : enums.keySet()) 232 reload(enumType); 233 } 234 235 /** 236 * Returns a <tt>Value</tt> object for the given enumeration type. 237 * This method first looks at the internal map to see if a value 238 * object already exists for <tt>enumType</tt>. If it does, it 239 * is returned. Otherwise a new value object is created, placed 240 * in the map, and returned. 241 * @param enumType an enumeration class. 242 * @return a value object of the given enumeration type. 243 */ 244 @SuppressWarnings("unchecked") 245 private <E extends Enum<E>> Value<E> getOrMakeMapValueFor(Class<E> enumType) 246 { 247 //The complier can't know what we know here: That the Value we get back 248 //for Class<E> really is Value<E>. We suppress this warning. 249 Value<E> mapValue = enums.get(enumType); 250 251 if (mapValue == null) 252 { 253 mapValue = new Value<E>(enumType); 254 enums.put(enumType, mapValue); 255 } 256 257 return mapValue; 258 } 259 260 //---------------------------------------------------------------------------- 261 // Text Representation 262 //---------------------------------------------------------------------------- 263 264 /** 265 * Returns a text representation of {@code element}. 266 * <p> 267 * The returned string will be in lower case, except that the first letter of 268 * each word will be in upper case. The string is based on the {@code name} 269 * of the element (as returned by {@code element.name()}). Any underscores 270 * in the name are replaced with spaces.</p> 271 * <p> 272 * The main users of this method will be the enumeration classes themselves.</p> 273 * 274 * @param element a member of an enumeration class. 275 * 276 * @return a text representation of {@code element}. If {@code element} is 277 * <i>null</i>, the empty string (<tt>""</tt>) is returned. 278 */ 279 public <E extends Enum<E>> String enumToString(E element) 280 { 281 if (element == null) 282 return ""; 283 284 String name = element.name().replaceAll("_", " "); 285 286 return StringUtil.getInstance() 287 .lowerAndCapitalizeFirstLetterOfEachWord(name); 288 } 289 290 /** 291 * Returns a text representation of {@code element}. 292 * <p> 293 * The returned string will be in Upper case. The string is based on the {@code name} 294 * of the element (as returned by {@code element.name()}). Any underscores 295 * in the name are replaced with spaces.</p> 296 * <p> 297 * The main users of this method will be the enumeration classes themselves.</p> 298 * 299 * @param element a member of an enumeration class. 300 * 301 * @return a text representation of {@code element}. If {@code element} is 302 * <i>null</i>, the empty string (<tt>""</tt>) is returned. 303 */ 304 public <E extends Enum<E>> String enumToUpperString(E element) 305 { 306 if (element == null) 307 return ""; 308 309 String name = element.name().replaceAll("_", " "); 310 311 return name; 312 313 } 314 315 316 /** 317 * Returns an enumeration constant from {@code enumType} represented by 318 * {@code text}. 319 * <p> 320 * Leading and trailing whitespace is first stripped from {@code text}. 321 * A case-insensitive comparison against the {@code name} and 322 * {@code toString} methods of each constant in the enumeration is then 323 * performed. If the enumeration class has a {@code getSymbol} method, 324 * it is also used in the comparison. If no match is found, <i>null</i> 325 * is returned.</p> 326 * 327 * @param enumType an enumeration class. 328 * 329 * @param text a text representation of a constant in {@code enumType}. 330 * 331 * @return an enumeration constant from {@code enumType}. 332 * 333 * @see #enumFromStringOrDefault(Class, String) 334 */ 335 public <E extends Enum<E>> E enumFromString(Class<E> enumType, String text) 336 { 337 //Quick exit if input class is null 338 if (enumType == null) 339 return null; 340 341 E result = null; 342 343 boolean hasSymbol = Symbolic.class.isAssignableFrom(enumType); 344 345 //Remove leading & trailing whitespace from text and handle null 346 text = StringUtil.getInstance().normalizeString(text); 347 348 E[] elements = enumType.getEnumConstants(); 349 350 //Loop through enumeration constants of input class 351 for (E element : elements) 352 { 353 //Try to match against standard name and toString methods 354 if (text.equalsIgnoreCase(element.name()) || 355 text.equalsIgnoreCase(element.toString())) 356 { 357 result = element; 358 break; 359 } 360 //If no match, and if the class has the getSymbol method, 361 //see if text matches symbol. 362 else if (hasSymbol) 363 { 364 Symbolic symbolHolder = (Symbolic)element; 365 366 //Cannot always ignore case for symbols (eg, milli-x vs Mega-x) 367 if ((symbolHolder.symbolsAreCaseSensitive() && 368 text.equals(symbolHolder.getSymbol())) 369 || 370 text.equalsIgnoreCase(symbolHolder.getSymbol())) 371 { 372 result = element; 373 break; 374 } 375 } 376 } 377 378 return result; 379 } 380 381 /** 382 * Returns an enumeration constant from {@code enumType} based on 383 * {@code text}. 384 * <p> 385 * This method is similar to {@link #enumFromString(Class, String)}, 386 * except that if no match is found, a default constant from 387 * {@code enumType} is returned.</p> 388 * <p> 389 * If {@code enumType} 390 * <ol> 391 * <li>is non-null,</li> 392 * <li>is an enumeration class, and</li> 393 * <li>has at least one constant</li> 394 * </ol> 395 * then the return value is guaranteed to be non-null.</p> 396 * 397 * @param enumType an enumeration class. 398 * 399 * @param text a text representation of a constant in {@code enumType}. 400 * 401 * @return an enumeration constant from {@code enumType}. 402 * 403 * @see #enumFromString(Class, String) 404 * @see #getDefaultValueFor(Class) 405 */ 406 public <E extends Enum<E>> E enumFromStringOrDefault(Class<E> enumType, String text) 407 { 408 E result = enumFromString(enumType, text); 409 410 //If there was no match, use default 411 if (result == null) 412 result = getDefaultValueFor(enumType); 413 414 return result; 415 } 416 417 //============================================================================ 418 // INTERNAL CLASSES 419 //============================================================================ 420 421 /** 422 * Holds those properties of an enumeration class that are dealt with by 423 * the enumeration utility. Instances of this class are held in the 424 * utility's map, using enumeration classes as the keys. 425 */ 426 class Value <E extends Enum<E>> 427 { 428 E dfault; 429 EnumSet<E> selectable; 430 EnumSet<E> complete; 431 432 /** 433 * Creates a new instance for the given enumeration type. 434 * This constructor ensures that all of the internal properties 435 * are set to non-null values. This constructor first looks 436 * for a properties file for the enumertation type. If one is 437 * found it is used to value some of the properties. If it is 438 * not found, or if it is found but does not contain a given 439 * property, that property(ies) is given a default value. 440 */ 441 Value(Class<E> enumType) 442 { 443 //See if we have a properties file for this class. 444 try 445 { 446 InputStream propFile = makePropFileFor(enumType); 447 Properties enumProps = new Properties(); 448 enumProps.load(propFile); 449 setValuesFrom(enumProps, enumType); 450 } 451 //If not, set the default values of the properties 452 catch (Exception ex) 453 { 454 setDefaultEnumFor(enumType); 455 setDefaultSelectableFor(enumType); 456 } 457 458 //Do not use the properties files for the complete set 459 complete = EnumSet.allOf(enumType); 460 } 461 462 /** 463 * 464 */ 465 private void setValuesFrom(Properties properties, Class<E> enumType) 466 { 467 //Set the default element 468 try { 469 dfault = Enum.valueOf(enumType, properties.getProperty("default")); 470 } 471 catch (Exception ex) { 472 setDefaultEnumFor(enumType); 473 } 474 475 //Set the selectable collection by first setting the default set 476 //and then removing from that collection any legal elements we 477 //find in the properties file. 478 setDefaultSelectableFor(enumType); 479 String nonselectables = 480 properties.getProperty("nonselectable").replaceAll("\\s", ""); 481 482 for (String nonselectable : nonselectables.split(",")) 483 { 484 try { 485 selectable.remove(Enum.valueOf(enumType, nonselectable)); 486 } 487 catch (Exception ex) { 488 // Do nothing but log that an error happend. 489 log.error("EnumerationUtility unable to match value. Possibly due to a typo in the properties file.",ex); 490 } 491 } 492 } 493 494 /** 495 * 496 */ 497 @SuppressWarnings("unchecked") 498 private void setDefaultEnumFor(Class<E> enumType) 499 { 500 dfault = null; 501 502 if (enumType != null) 503 { 504 //See if class has a (static) getDefault() method 505 try 506 { 507 Method getDefault = enumType.getDeclaredMethod("getDefault", 508 (Class[])null); 509 dfault = (E)getDefault.invoke(enumType, (Object[])null); 510 } 511 //If trouble w/ getDefault, use first element in list 512 catch (Exception ex) 513 { 514 E[] elements = enumType.getEnumConstants(); 515 if (elements.length > 0) 516 dfault = elements[0]; 517 } 518 } 519 } 520 521 /** 522 * 523 */ 524 private void setDefaultSelectableFor(Class<E> enumType) 525 { 526 selectable = EnumSet.allOf(enumType); 527 } 528 529 /** 530 * 531 */ 532 private InputStream makePropFileFor(Class<E> enumType) 533 { 534 return enumType.getResourceAsStream(makePropFileNameFor(enumType)); 535 } 536 537 /** 538 * 539 */ 540 private String makePropFileNameFor(Class<E> enumType) 541 { 542 return enumType.getSimpleName() + '.' + "properties"; 543 } 544 } 545 546 //============================================================================ 547 // 548 //============================================================================ 549 550 /* public static void main(String[] args) 551 { 552 EnumerationUtility util = new EnumerationUtility(); 553 554 System.out.println("DistanceUnits: " + util.getSelectableSetFor(edu.nrao.sss.measure.DistanceUnits.class)); 555 System.out.println("DistanceUnits.default: " + util.getDefaultValueFor(edu.nrao.sss.measure.DistanceUnits.class)); 556 System.out.println("TimeUnits: " + util.getSelectableSetFor(edu.nrao.sss.measure.TimeUnits.class)); 557 System.out.println("TimeUnits.default: " + util.getDefaultValueFor(Tedu.nrao.sss.measure.imeUnits.class)); 558 559 util.setDefaultValue(edu.nrao.sss.measure.TimeUnits.class, edu.nrao.sss.measure.TimeUnits.HOUR); 560 System.out.println("TimeUnits.default: " + util.getDefaultValueFor(edu.nrao.sss.measure.TimeUnits.class)); 561 }*/ 562 /* 563 public static void main(String[] args) 564 { 565 EnumerationUtility util = new EnumerationUtility(); 566 567 //Trigger loading of classes 568 util.getDefaultValueFor(edu.nrao.sss.measure.DistanceUnits.class); 569 util.getDefaultValueFor(edu.nrao.sss.measure.ArcUnits.class); 570 571 System.out.println("Util has " + util.enums.size() + " mappings."); 572 System.out.println(); 573 574 for (Class c : util.enums.keySet()) 575 { 576 System.out.println("Class " + c.getSimpleName()); 577 Value v = util.enums.get(c); 578 System.out.println(" Default: " + v.dfault); 579 System.out.print(" Selectable:"); 580 for (Object e : v.selectable) 581 System.out.print(" " + e + ","); 582 System.out.println(); 583 System.out.println(); 584 } 585 586 System.out.println(); 587 588 for (edu.nrao.sss.model.project.scan.ScanIntent intent : 589 edu.nrao.sss.model.project.scan.ScanIntent.values()) 590 System.out.println(intent.name() + " -> " + util.enumToString(intent)); 591 } 592 593 public static void main(String[] args) 594 { 595 EnumerationUtility util = EnumerationUtility.getSharedInstance(); 596 Class<Epoch> enumType = Epoch.class; 597 int[] counts = new int[util.getCompleteSetFor(enumType).size()]; 598 599 for (int i=0; i < 100000; i++) 600 { 601 counts[util.getRandomValueFor(enumType).ordinal()]++; 602 } 603 604 for (int c=0; c < counts.length; c++) 605 System.out.println("Count["+c+"] = " + counts[c]); 606 } 607 */ 608 }