001 package hirondelle.web4j.util; 002 003 import java.util.*; 004 import java.util.logging.*; 005 import java.util.regex.*; 006 import java.math.BigDecimal; 007 import java.text.CharacterIterator; 008 import java.text.StringCharacterIterator; 009 import java.lang.reflect.Array; 010 import java.lang.reflect.InvocationTargetException; 011 import java.lang.reflect.Method; 012 import hirondelle.web4j.security.SafeText; 013 import hirondelle.web4j.model.Decimal; 014 015 /** 016 Static convenience methods for common tasks, which eliminate code duplication. 017 018 <P>{@link Args} wraps certain methods of this class into a form suitable for checking arguments. 019 020 <P>{@link hirondelle.web4j.util.WebUtil} includes utility methods particular to web applications. 021 */ 022 public final class Util { 023 024 /** 025 Return true only if <tt>aNumEdits</tt> is greater than <tt>0</tt>. 026 027 <P>This method is intended for database operations. 028 */ 029 public static boolean isSuccess(int aNumEdits){ 030 return aNumEdits > 0; 031 } 032 033 /** 034 Return <tt>true</tt> only if <tt>aText</tt> is not null, 035 and is not empty after trimming. (Trimming removes both 036 leading/trailing whitespace and ASCII control characters. See {@link String#trim()}.) 037 038 <P> For checking argument validity, {@link Args#checkForContent} should 039 be used instead of this method. 040 041 @param aText possibly-null. 042 */ 043 public static boolean textHasContent(String aText) { 044 return (aText != null) && (aText.trim().length() > 0); 045 } 046 047 /** 048 Return <tt>true</tt> only if <tt>aText</tt> is not null, 049 and if its raw <tt>String</tt> is not empty after trimming. (Trimming removes both 050 leading/trailing whitespace and ASCII control characters. See {@link String#trim()}.) 051 052 @param aText possibly-null. 053 */ 054 public static boolean textHasContent(SafeText aText){ 055 return (aText != null) && (aText.getRawString().trim().length() > 0); 056 } 057 058 /** 059 If <tt>aText</tt> is null, return null; else return <tt>aText.trim()</tt>. 060 061 This method is especially useful for Model Objects whose <tt>String</tt> 062 parameters to its constructor can take any value whatsoever, including 063 <tt>null</tt>. Using this method lets <tt>null</tt> params remain 064 <tt>null</tt>, while trimming all others. 065 066 @param aText possibly-null. 067 */ 068 public static String trimPossiblyNull(String aText){ 069 return aText == null ? null : aText.trim(); 070 } 071 072 /** 073 Return <tt>true</tt> only if <tt>aNumber</tt> is in the range 074 <tt>aLow..aHigh</tt> (inclusive). 075 076 <P> For checking argument validity, {@link Args#checkForRange} should 077 be used instead of this method. 078 079 @param aLow less than or equal to <tt>aHigh</tt>. 080 */ 081 static public boolean isInRange( int aNumber, int aLow, int aHigh ){ 082 if (aLow > aHigh) { 083 throw new IllegalArgumentException( 084 "Low: " + aLow + " is greater than High: " + aHigh 085 ); 086 } 087 return (aLow <= aNumber && aNumber <= aHigh); 088 } 089 090 /** 091 Return <tt>true</tt> only if the number of decimal places in <tt>aAmount</tt> is in the range 092 0..<tt>aMaxNumDecimalPlaces</tt> (inclusive). 093 094 @param aAmount any amount, positive or negative.. 095 @param aMaxNumDecimalPlaces is <tt>1</tt> or more. 096 */ 097 static public boolean hasMaxDecimals(BigDecimal aAmount, int aMaxNumDecimalPlaces){ 098 Args.checkForPositive(aMaxNumDecimalPlaces); 099 int numDecimals = aAmount.scale(); 100 return 0 <= numDecimals && numDecimals <= aMaxNumDecimalPlaces; 101 } 102 103 /** 104 Return <tt>true</tt> only if the number of decimal places in <tt>aAmount</tt> is in the range 105 0..<tt>aMaxNumDecimalPlaces</tt> (inclusive). 106 107 @param aAmount any amount, positive or negative.. 108 @param aMaxNumDecimalPlaces is <tt>1</tt> or more. 109 */ 110 static public boolean hasMaxDecimals(Decimal aAmount, int aMaxNumDecimalPlaces){ 111 return hasMaxDecimals(aAmount.getAmount(), aMaxNumDecimalPlaces); 112 } 113 114 /** 115 Return <tt>true</tt> only if <tt>aAmount</tt> has exactly the number 116 of specified decimals. 117 118 @param aNumDecimals is 0 or more. 119 */ 120 public static boolean hasNumDecimals(BigDecimal aAmount, int aNumDecimals){ 121 if( aNumDecimals < 0 ){ 122 throw new IllegalArgumentException("Number of decimals must be 0 or more: " + quote(aNumDecimals)); 123 } 124 return aAmount.scale() == aNumDecimals; 125 } 126 127 /** 128 Return <tt>true</tt> only if <tt>aAmount</tt> has exactly the number 129 of specified decimals. 130 131 @param aNumDecimals is 0 or more. 132 */ 133 public static boolean hasNumDecimals(Decimal aAmount, int aNumDecimals){ 134 if( aNumDecimals < 0 ){ 135 throw new IllegalArgumentException("Number of decimals must be 0 or more: " + quote(aNumDecimals)); 136 } 137 return aAmount.getAmount().scale() == aNumDecimals; 138 } 139 140 /** 141 Parse text commonly used to denote booleans into a {@link Boolean} object. 142 143 <P>The parameter passed to this method is first trimmed (if it is non-null), 144 and then compared to the following Strings, ignoring case : 145 <ul> 146 <li>{@link Boolean#TRUE} : 'true', 'yes', 'on' 147 <li>{@link Boolean#FALSE} : 'false', 'no', 'off' 148 </ul> 149 150 <P>Any other text will cause a <tt>RuntimeException</tt>. (Note that this behavior 151 is different from that of {@link Boolean#valueOf(String)}). 152 153 <P>(This method is clearly biased in favor of English text. It is hoped that this is not too inconvenient for the caller.) 154 155 @param aBooleanAsText possibly-null text to be converted into a {@link Boolean}; if null, then the 156 return value is null. 157 */ 158 public static Boolean parseBoolean(String aBooleanAsText){ 159 Boolean result = null; 160 String value = trimPossiblyNull(aBooleanAsText); 161 if ( value == null ) { 162 //do nothing - return null 163 } 164 else if ( value.equalsIgnoreCase("false") || value.equalsIgnoreCase("no") || value.equalsIgnoreCase("off") ) { 165 result = Boolean.FALSE; 166 } 167 else if ( value.equalsIgnoreCase("true") || value.equalsIgnoreCase("yes") || value.equalsIgnoreCase("on") ) { 168 result = Boolean.TRUE; 169 } 170 else { 171 throw new IllegalArgumentException( 172 "Cannot parse into Boolean: " + quote(aBooleanAsText) + ". Accepted values are: true/false/yes/no/on/off" 173 ); 174 } 175 return result; 176 } 177 178 /** 179 Coerce a possibly-<tt>null</tt> {@link Boolean} value into {@link Boolean#FALSE}. 180 181 <P>This method is usually called in Model Object constructors that have two-state <tt>Boolean</tt> fields. 182 183 <P>This method is supplied specifically for request parameters that may be <em>missing</em> from the request, 184 during normal operation of the program. 185 <P>Example : a form has a checkbox for 'yes, send me your newsletter', and the data is modeled has having two states - 186 <tt>true</tt> and <tt>false</tt>. If the checkbox is <em>not checked</em> , however, the browser will 187 likely not POST any corresponding request parameter - it will be <tt>null</tt>. In that case, calling this method 188 will coerce such <tt>null</tt> parameters into {@link Boolean#FALSE}. 189 190 <P>There are other cases in which data is modeled as having not two states, but <em>three</em> : 191 <tt>true</tt>, <tt>false</tt>, and <tt>null</tt>. The <tt>null</tt> value usually means 'unknown'. 192 In that case, this method should <em>not</em> be called. 193 */ 194 public static Boolean nullMeansFalse(Boolean aBoolean){ 195 return aBoolean == null ? Boolean.FALSE : aBoolean; 196 } 197 198 /** 199 Create a {@link Pattern} corresponding to a <tt>List</tt>. 200 201 Example: if the {@link List} contains "cat" and "dog", then the returned 202 <tt>Pattern</tt> will correspond to the regular expression "(cat|dog)". 203 204 @param aList is not empty, and contains objects whose <tt>toString()</tt> 205 value represents each item in the pattern. 206 */ 207 public static final Pattern getPatternFromList( List<?> aList ){ 208 if ( aList.isEmpty() ){ 209 throw new IllegalArgumentException(); 210 } 211 StringBuilder regex = new StringBuilder("("); 212 Iterator<?> iter = aList.iterator(); 213 while (iter.hasNext()){ 214 Object item = iter.next(); 215 regex.append( item.toString() ); 216 if ( iter.hasNext() ) { 217 regex.append( "|" ); 218 } 219 } 220 regex.append(")"); 221 return Pattern.compile( regex.toString() ); 222 } 223 224 /** 225 Return <tt>true</tt> only if <tt>aMoney</tt> equals <tt>0</tt> 226 or <tt>0.00</tt>. 227 */ 228 public static boolean isZeroMoney( BigDecimal aMoney ){ 229 final BigDecimal ZERO_MONEY = new BigDecimal("0"); 230 final BigDecimal ZERO_MONEY_WITH_DECIMAL = new BigDecimal("0.00"); 231 return 232 aMoney.equals(ZERO_MONEY) || 233 aMoney.equals(ZERO_MONEY_WITH_DECIMAL) 234 ; 235 } 236 237 /** 238 Return true only if <tt>aText</tt> is non-null, and matches 239 <tt>aPattern</tt>. 240 241 <P>Differs from {@link Pattern#matches} and {@link String#matches}, 242 since the regex argument is a compiled {@link Pattern}, not a 243 <tt>String</tt>. 244 */ 245 public static boolean matches(Pattern aPattern, String aText){ 246 /* 247 Implementation Note: 248 Patterns are thread-safe, while Matchers are not. Thus, a Pattern may 249 be compiled by a class once upon startup, then reused safely in a 250 multi-threaded environment. 251 */ 252 if (aText == null) return false; 253 Matcher matcher = aPattern.matcher(aText); 254 return matcher.matches(); 255 } 256 257 /** 258 Return true only if <tt>aText</tt> is non-null, and contains 259 a substring that matches <tt>aPattern</tt>. 260 */ 261 public static boolean contains(Pattern aPattern, String aText){ 262 if (aText == null) return false; 263 Matcher matcher = aPattern.matcher(aText); 264 return matcher.find(); 265 } 266 267 /** 268 If <tt>aPossiblyNullItem</tt> is <tt>null</tt>, then return <tt>aReplacement</tt> ; 269 otherwise return <tt>aPossiblyNullItem</tt>. 270 271 <P>Intended mainly for occasional use in Model Object constructors. It is used to 272 coerce <tt>null</tt> items into a more appropriate default value. 273 */ 274 public static <E> E replaceIfNull(E aPossiblyNullItem, E aReplacement){ 275 return aPossiblyNullItem == null ? aReplacement : aPossiblyNullItem; 276 } 277 278 /** 279 <P>Convert end-user input into a form suitable for {@link BigDecimal}. 280 281 <P>The idea is to allow a wide range of user input formats for monetary amounts. For 282 example, an amount may be input as <tt>'$1,500.00'</tt>, <tt>'U$1500.00'</tt>, 283 or <tt>'1500.00 U$'</tt>. These entries can all be converted into a 284 <tt>BigDecimal</tt> by simply stripping out all characters except for digits 285 and the decimal character. 286 287 <P>Removes all characters from <tt>aCurrencyAmount</tt> which are not digits or 288 <tt>aDecimalSeparator</tt>. Finally, if <tt>aDecimalSeparator</tt> is not 289 a period (expected by <tt>BigDecimal</tt>) then it is replaced with a period. 290 291 @param aDecimalSeparator must have content, and must have length of <tt>1</tt>. 292 */ 293 static public String trimCurrency(String aCurrencyAmount, String aDecimalSeparator){ 294 Args.checkForContent(aDecimalSeparator); 295 if ( aDecimalSeparator.length() != 1) { 296 throw new IllegalArgumentException( 297 "Decimal separator is not a single character: " + Util.quote(aDecimalSeparator) 298 ); 299 } 300 301 StringBuilder result = new StringBuilder(); 302 StringCharacterIterator iter = new StringCharacterIterator(aCurrencyAmount); 303 char character = iter.current(); 304 while (character != CharacterIterator.DONE){ 305 if ( Character.isDigit(character) ){ 306 result.append(character); 307 } 308 else if (aDecimalSeparator.charAt(0) == character){ 309 result.append(Consts.PERIOD.charAt(0)); 310 } 311 else { 312 //do not append any other chars 313 } 314 character = iter.next(); 315 } 316 return result.toString(); 317 } 318 319 /** 320 Return a {@link Logger} whose name follows a specific naming convention. 321 322 <P>The conventional logger names used by WEB4J are taken as 323 <tt>aClass.getPackage().getName()</tt>. 324 325 <P>Logger names appearing in the <tt>logging.properties</tt> config file 326 must match the names returned by this method. 327 328 <P>If an application requires an alternate naming convention, then an 329 alternate implementation can be easily constructed. Alternate naming conventions 330 might account for : 331 <ul> 332 <li>pre-pending the logger name with the name of the application (this is useful 333 where log handlers are shared between different applications) 334 <li>adding version information 335 </ul> 336 */ 337 public static Logger getLogger(Class<?> aClass){ 338 return Logger.getLogger(aClass.getPackage().getName()); 339 } 340 341 /** 342 Call {@link String#valueOf(Object)} on <tt>aObject</tt>, and place the result in single quotes. 343 <P>This method is a bit unusual in that it can accept a <tt>null</tt> 344 argument : if <tt>aObject</tt> is <tt>null</tt>, it will return <tt>'null'</tt>. 345 346 <P>This method reduces errors from leading and trailing spaces, by placing 347 single quotes around the returned text. Such leading and trailing spaces are 348 both easy to create and difficult to detect (a bad combination). 349 350 <P>Note that such quotation is likely needed only for <tt>String</tt> data, 351 since trailing or leading spaces will not occur for other types of data. 352 */ 353 public static String quote(Object aObject){ 354 String result = "EXCEPTION OCCURED"; 355 try { 356 result = Consts.SINGLE_QUOTE + String.valueOf(aObject) + Consts.SINGLE_QUOTE; 357 } 358 catch (Throwable ex){ 359 //errors were seen in this branch - depends on the impl of the passed object/array 360 //do nothing - use the default 361 } 362 return result; 363 } 364 365 /** 366 Remove any initial or final quote characters from <tt>aText</tt>, either a single quote or 367 a double quote. 368 369 <P>This method will not trim the text passed to it. Furthermore, it will examine only 370 the very first character and the very last character. It will remove the first or last character, 371 but only if they are a single quote or a double quote. 372 373 <P>If <tt>aText</tt> has no content, then it is simply returned by this method, as is, 374 including possibly-<tt>null</tt> values. 375 @param aText is possibly <tt>null</tt>, and is not trimmed by this method 376 */ 377 public static String removeQuotes(String aText){ 378 String result = null; 379 if ( ! textHasContent(aText)) { 380 result = aText; 381 } 382 else { 383 int length = aText.length(); 384 String firstChar = aText.substring(0,1); 385 String lastChar = aText.substring(length-1); 386 boolean startsWithQuote = firstChar.equalsIgnoreCase("\"") || firstChar.equalsIgnoreCase("'"); 387 boolean endsWithQuote = lastChar.equalsIgnoreCase("\"") || lastChar.equalsIgnoreCase("'"); 388 int startIdx = startsWithQuote ? 1 : 0; 389 int endIdx = endsWithQuote ? length-1 : length; 390 result = aText.substring(startIdx, endIdx); 391 } 392 return result; 393 } 394 395 /** 396 Ensure the initial character of <tt>aText</tt> is capitalized. 397 398 <P>Does not trim <tt>aText</tt>. 399 400 @param aText has content. 401 */ 402 public static String withInitialCapital(String aText) { 403 Args.checkForContent(aText); 404 final int FIRST = 0; 405 final int ALL_BUT_FIRST = 1; 406 StringBuilder result = new StringBuilder(); 407 result.append( Character.toUpperCase(aText.charAt(FIRST)) ); 408 result.append(aText.substring(ALL_BUT_FIRST)); 409 return result.toString(); 410 } 411 412 /** 413 Ensure <tt>aText</tt> contains no spaces. 414 415 <P>Along with {@link #withInitialCapital(String)}, this method is useful for 416 mapping request parameter names into corresponding <tt>getXXX</tt> methods. 417 For example, the text <tt>'Email Address'</tt> and <tt>'emailAddress'</tt> 418 can <em>both</em> be mapped to a method named <tt>'getEmailAddress()'</tt>, by using : 419 <PRE> 420 String methodName = "get" + Util.withNoSpaces(Util.withInitialCapital(name)); 421 </PRE> 422 423 @param aText has content 424 */ 425 public static String withNoSpaces(String aText){ 426 Args.checkForContent(aText); 427 return replace(aText.trim(), Consts.SPACE, Consts.EMPTY_STRING); 428 } 429 430 /** 431 Replace every occurrence of a fixed substring with substitute text. 432 433 <P>This method is distinct from {@link String#replaceAll}, since it does not use a 434 regular expression. 435 436 @param aInput may contain substring <tt>aOldText</tt>; satisfies {@link #textHasContent(String)} 437 @param aOldText substring which is to be replaced; possibly empty, but never null 438 @param aNewText replacement for <tt>aOldText</tt>; possibly empty, but never null 439 */ 440 public static String replace(String aInput, String aOldText, String aNewText){ 441 if ( ! textHasContent(aInput) ) { 442 throw new IllegalArgumentException("Input must have content."); 443 } 444 if ( aNewText == null ) { 445 throw new NullPointerException("Replacement text may be empty, but never null."); 446 } 447 final StringBuilder result = new StringBuilder(); 448 //startIdx and idxOld delimit various chunks of aInput; these 449 //chunks always end where aOldText begins 450 int startIdx = 0; 451 int idxOld = 0; 452 while ((idxOld = aInput.indexOf(aOldText, startIdx)) >= 0) { 453 //grab a part of aInput which does not include aOldPattern 454 result.append( aInput.substring(startIdx, idxOld) ); 455 //add replacement text 456 result.append( aNewText ); 457 //reset the startIdx to just after the current match, to see 458 //if there are any further matches 459 startIdx = idxOld + aOldText.length(); 460 } 461 //the final chunk will go to the end of aInput 462 result.append( aInput.substring(startIdx) ); 463 return result.toString(); 464 } 465 466 /** 467 Return a <tt>String</tt> suitable for logging, having one item from <tt>aCollection</tt> 468 per line. 469 470 <P>For the <tt>Collection</tt> containing <br> 471 <tt>[null, "Zebra", "aardvark", "Banana", "", "aardvark", new BigDecimal("5.00")]</tt>, 472 473 <P>the return value is : 474 <PRE> 475 (7) { 476 '' 477 '5.00' 478 'aardvark' 479 'aardvark' 480 'Banana' 481 'null' 482 'Zebra' 483 } 484 </PRE> 485 486 <P>The text for each item is generated by calling {@link #quote}, and by appending a new line. 487 488 <P>As well, this method reports the total number of items, <em>and places items in 489 alphabetical order</em> (ignoring case). (The iteration order of the <tt>Collection</tt> 490 passed by the caller will often differ from the order of items presented in the return value.) 491 </PRE> 492 */ 493 public static String logOnePerLine(Collection<?> aCollection){ 494 int STANDARD_INDENTATION = 1; 495 return logOnePerLine(aCollection, STANDARD_INDENTATION); 496 } 497 498 /** 499 As in {@link #logOnePerLine(Collection)}, but with specified indentation level. 500 501 @param aIndentLevel greater than or equal to 1, acts as multiplier for a 502 "standard" indentation level of two spaces. 503 */ 504 public static String logOnePerLine(Collection<?> aCollection, int aIndentLevel){ 505 Args.checkForPositive(aIndentLevel); 506 String indent = getIndentation(aIndentLevel); 507 StringBuilder result = new StringBuilder(); 508 result.append("(" + aCollection.size() + ") {" + Consts.NEW_LINE); 509 List<String> lines = new ArrayList<String>(aCollection.size()); 510 for (Object item: aCollection){ 511 StringBuilder line = new StringBuilder(indent); 512 line.append( quote(item) ); //nulls ok 513 line.append( Consts.NEW_LINE ); 514 lines.add(line.toString()); 515 } 516 addSortedLinesToResult(result, lines); 517 result.append(getFinalIndentation(aIndentLevel)); 518 result.append("}"); 519 return result.toString(); 520 } 521 522 /** 523 Return a <tt>String</tt> suitable for logging, having one item from <tt>aMap</tt> 524 per line. 525 526 <P>For a <tt>Map</tt> containing <br> 527 <tt>["b"="blah", "a"=new BigDecimal(5.00), "Z"=null, null=new Integer(3)]</tt>, 528 529 <P>the return value is : 530 <PRE> 531 (4) { 532 'a' = '5.00' 533 'b' = 'blah' 534 'null' = '3' 535 'Z' = 'null' 536 } 537 </PRE> 538 539 <P>The text for each key and value is generated by calling {@link #quote}, and 540 appending a new line after each entry. 541 542 <P>As well, this method reports the total number of items, <em>and places items in 543 alphabetical order of their keys</em> (ignoring case). (The iteration order of the 544 <tt>Map</tt> passed by the caller will often differ from the order of items in the 545 return value.) 546 547 <P>An attempt is made to suppress the emission of passwords. Values in a Map are 548 presented as <tt>****</tt> if the following conditions are all true : 549 <ul> 550 <li>{@link String#valueOf(java.lang.Object)} applied to the <em>key</em> contains the word <tt>password</tt> or 551 <tt>credential</tt> (ignoring case) 552 <li>the <em>value</em> is not an array or a <tt>Collection</tt> 553 </ul> 554 */ 555 public static String logOnePerLine(Map<?,?> aMap){ 556 StringBuilder result = new StringBuilder(); 557 result.append("(" + aMap.size() + ") {" + Consts.NEW_LINE); 558 List<String> lines = new ArrayList<String>(aMap.size()); 559 String SEPARATOR = " = "; 560 Iterator iter = aMap.keySet().iterator(); 561 while ( iter.hasNext() ){ 562 Object key = iter.next(); 563 StringBuilder line = new StringBuilder(INDENT); 564 line.append(quote(key)); //nulls ok 565 line.append(SEPARATOR); 566 Object value = aMap.get(key); 567 int MORE_INDENTATION = 2; 568 if ( value != null && value instanceof Collection) { 569 line.append( logOnePerLine((Collection)value, MORE_INDENTATION)); 570 } 571 else if ( value != null && value.getClass().isArray() ){ 572 List valueItems = Arrays.asList( (Object[])value ); 573 line.append( logOnePerLine(valueItems, MORE_INDENTATION) ); 574 } 575 else { 576 value = suppressPasswords(key, value); 577 line.append(quote(value)); //nulls ok 578 } 579 line.append(Consts.NEW_LINE); 580 lines.add(line.toString()); 581 } 582 addSortedLinesToResult(result, lines); 583 result.append("}"); 584 return result.toString(); 585 } 586 587 /** 588 Return a {@link Locale} object by parsing <tt>aRawLocale</tt>. 589 590 <P>The format of <tt>aRawLocale</tt> follows the 591 <tt>language_country_variant</tt> style used by {@link Locale}. The value is <i>not</i> checked against 592 {@link Locale#getAvailableLocales()}. 593 */ 594 public static Locale buildLocale(String aRawLocale){ 595 int language = 0; 596 int country = 1; 597 int variant = 2; 598 Locale result = null; 599 fLogger.finest("Raw Locale: " + aRawLocale); 600 String[] parts = aRawLocale.split("_"); 601 if (parts.length == 1) { 602 result = new Locale( parts[language] ); 603 } 604 else if (parts.length == 2) { 605 result = new Locale( parts[language], parts[country] ); 606 } 607 else if (parts.length == 3 ) { 608 result = new Locale( parts[language], parts[country], parts[variant] ); 609 } 610 else { 611 throw new AssertionError("Locale identifer has unexpected format: " + aRawLocale); 612 } 613 fLogger.finest("Parsed Locale : " + Util.quote(result.toString())); 614 return result; 615 } 616 617 /** 618 Return a {@link TimeZone} corresponding to a given {@link String}. 619 620 <P>If the given <tt>String</tt> does not correspond to a known <tt>TimeZone</tt> id, 621 as determined by {@link TimeZone#getAvailableIDs()}, then a 622 runtime exception is thrown. (This differs from the behavior of the 623 {@link TimeZone} class itself, and is the reason why this method exists.) 624 */ 625 public static TimeZone buildTimeZone(String aTimeZone) { 626 TimeZone result = null; 627 List<String> timeZones = Arrays.asList(TimeZone.getAvailableIDs()); 628 if( timeZones.contains(aTimeZone.trim()) ) { 629 result = TimeZone.getTimeZone(aTimeZone.trim()); 630 } 631 else { 632 fLogger.severe("Unknown Time Zone : " + quote(aTimeZone)); 633 //fLogger.severe("Known Time Zones : " + logOnePerLine(timeZones)); 634 throw new IllegalArgumentException("Unknown TimeZone Id : " + quote(aTimeZone)); 635 } 636 return result; 637 } 638 639 /** 640 Convenience method for producing a simple textual 641 representation of an array. 642 643 <P>The format of the returned {@link String} is the same as 644 {@link java.util.AbstractCollection#toString} : 645 <ul> 646 <li>non-empty array: <tt>[blah, blah]</tt> 647 <li>empty array: <tt>[]</tt> 648 <li>null array: <tt>null</tt> 649 </ul> 650 651 <P>Thanks to Jerome Lacoste for improving the implementation of this method. 652 653 @param aArray is a possibly-null array whose elements are 654 primitives or objects. Arrays of arrays are also valid, in which case 655 <tt>aArray</tt> is rendered in a nested, recursive fashion. 656 657 */ 658 public static String getArrayAsString(Object aArray){ 659 final String fSTART_CHAR = "["; 660 final String fEND_CHAR = "]"; 661 final String fSEPARATOR = ", "; 662 final String fNULL = "null"; 663 664 if ( aArray == null ) return fNULL; 665 checkObjectIsArray(aArray); 666 667 StringBuilder result = new StringBuilder( fSTART_CHAR ); 668 int length = Array.getLength(aArray); 669 for ( int idx = 0 ; idx < length ; ++idx ) { 670 Object item = Array.get(aArray, idx); 671 if ( isNonNullArray(item) ){ 672 //recursive call! 673 result.append( getArrayAsString(item) ); 674 } 675 else{ 676 result.append( item ); 677 } 678 if ( ! isLastItem(idx, length) ) { 679 result.append(fSEPARATOR); 680 } 681 } 682 result.append(fEND_CHAR); 683 return result.toString(); 684 } 685 686 /** 687 Transform a <tt>List</tt> into a <tt>Map</tt>. 688 689 <P>This method exists because it is sometimes necessary to transform a 690 <tt>List</tt> into a lookup table of some sort, using <em>unique</em> keys already 691 present in the <tt>List</tt> data. 692 693 <P>The <tt>List</tt> to be transformed contains objects having a method named 694 <tt>aKeyMethodName</tt>, and which returns objects of class <tt>aKeyClass</tt>. 695 Thus, data is extracted from each object to act as its key. Furthermore, the 696 key must be <em>unique</em>. If any duplicates are detected, then an 697 exception is thrown. This ensures that the returned <tt>Map</tt> will be the 698 same size as the given <tt>List</tt>, and that no data is silently discarded. 699 700 <P>The iteration order of the returned <tt>Map</tt> is identical to the iteration order of 701 the input <tt>List</tt>. 702 */ 703 public static <K,V> Map<K,V> asMap(List<V> aList, Class<K> aClass, String aKeyMethodName){ 704 Map<K,V> result = new LinkedHashMap<K,V>(); 705 for(V value: aList){ 706 K key = getMethodValue(value, aClass, aKeyMethodName); 707 if( result.containsKey(key) ){ 708 throw new IllegalArgumentException("Key must be unique. Duplicate detected : " + quote(key)); 709 } 710 result.put(key, value); 711 } 712 return result; 713 } 714 715 /** 716 Reverse the keys and values in a <tt>Map</tt>. 717 718 <P>This method exists because sometimes a lookup operation needs to be performed in a 719 style opposite to an existing <tt>Map</tt>. 720 721 <P>There is an unusual requirement on the <tt>Map</tt> argument: the map <em>values</em> must be 722 unique. Thus, the returned <tt>Map</tt> will be the same size as the input <tt>Map</tt>. If any 723 duplicates are detected, then an exception is thrown. 724 725 <P>The iteration order of the returned <tt>Map</tt> is identical to the iteration order of 726 the input <tt>Map</tt>. 727 */ 728 public static <K,V> Map<V,K> reverseMap(Map<K,V> aMap){ 729 Map<V,K> result = new LinkedHashMap<V,K>(); 730 for(Map.Entry<K,V> entry: aMap.entrySet()){ 731 if( result.containsKey(entry.getValue())){ 732 throw new IllegalArgumentException("Value must be unique. Duplicate detected : " + quote(entry.getValue())); 733 } 734 result.put(entry.getValue(), entry.getKey()); 735 } 736 return result; 737 } 738 739 740 // PRIVATE // 741 742 private Util(){ 743 //empty - prevents construction by the caller. 744 } 745 746 private static final Logger fLogger = Util.getLogger(Util.class); 747 private static final String INDENT = Consts.SPACE + Consts.SPACE; 748 private static final Pattern PASSWORD = Pattern.compile("(password|credential)", Pattern.CASE_INSENSITIVE); 749 750 private static void addSortedLinesToResult(StringBuilder aResult, List<String> aLines) { 751 Collections.sort(aLines, String.CASE_INSENSITIVE_ORDER); 752 for (String line: aLines){ 753 aResult.append( line ); 754 } 755 } 756 757 private static String getIndentation(int aIndentLevel){ 758 StringBuilder result = new StringBuilder(); 759 for (int idx = 1; idx <= aIndentLevel; ++idx){ 760 result.append(INDENT); 761 } 762 return result.toString(); 763 } 764 765 private static String getFinalIndentation(int aIndentLevel){ 766 return getIndentation(aIndentLevel - 1); 767 } 768 769 /** 770 Replace likely password values with a fixed string. 771 */ 772 private static Object suppressPasswords(Object aKey, Object aValue){ 773 Object result = aValue; 774 String key = String.valueOf(aKey); 775 Matcher matcher = PASSWORD.matcher(key); 776 if ( matcher.find() ){ 777 result = "*****"; 778 } 779 return result; 780 } 781 782 private static void checkObjectIsArray(Object aArray){ 783 if ( ! aArray.getClass().isArray() ) { 784 throw new IllegalArgumentException("Object is not an array."); 785 } 786 } 787 788 private static boolean isNonNullArray(Object aItem){ 789 return aItem != null && aItem.getClass().isArray(); 790 } 791 792 private static boolean isLastItem(int aIdx, int aLength){ 793 return (aIdx == aLength - 1); 794 } 795 796 private static <K> K getMethodValue(Object aValue, Class<K> aClass, String aKeyMethodName){ 797 K result = null; 798 try { 799 Method method = aValue.getClass().getMethod(aKeyMethodName); //no args 800 result = (K)method.invoke(aValue); 801 } 802 catch (NoSuchMethodException ex){ 803 handleInvocationEx(aValue.getClass(), aKeyMethodName); 804 } 805 catch (IllegalAccessException ex){ 806 handleInvocationEx(aValue.getClass(), aKeyMethodName); 807 } 808 catch (InvocationTargetException ex){ 809 handleInvocationEx(aValue.getClass(), aKeyMethodName); 810 } 811 return result; 812 } 813 814 private static void handleInvocationEx(Class<?> aClass, String aKeyMethodName){ 815 throw new IllegalArgumentException("Cannot invoke method named " + quote(aKeyMethodName) + " on object of class " + quote(aClass)); 816 } 817 }