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 return Consts.SINGLE_QUOTE + String.valueOf(aObject) + Consts.SINGLE_QUOTE;
355 }
356
357 /**
358 Remove any initial or final quote characters from <tt>aText</tt>, either a single quote or
359 a double quote.
360
361 <P>This method will not trim the text passed to it. Furthermore, it will examine only
362 the very first character and the very last character. It will remove the first or last character,
363 but only if they are a single quote or a double quote.
364
365 <P>If <tt>aText</tt> has no content, then it is simply returned by this method, as is,
366 including possibly-<tt>null</tt> values.
367 @param aText is possibly <tt>null</tt>, and is not trimmed by this method
368 */
369 public static String removeQuotes(String aText){
370 String result = null;
371 if ( ! textHasContent(aText)) {
372 result = aText;
373 }
374 else {
375 int length = aText.length();
376 String firstChar = aText.substring(0,1);
377 String lastChar = aText.substring(length-1);
378 boolean startsWithQuote = firstChar.equalsIgnoreCase("\"") || firstChar.equalsIgnoreCase("'");
379 boolean endsWithQuote = lastChar.equalsIgnoreCase("\"") || lastChar.equalsIgnoreCase("'");
380 int startIdx = startsWithQuote ? 1 : 0;
381 int endIdx = endsWithQuote ? length-1 : length;
382 result = aText.substring(startIdx, endIdx);
383 }
384 return result;
385 }
386
387 /**
388 Ensure the initial character of <tt>aText</tt> is capitalized.
389
390 <P>Does not trim <tt>aText</tt>.
391
392 @param aText has content.
393 */
394 public static String withInitialCapital(String aText) {
395 Args.checkForContent(aText);
396 final int FIRST = 0;
397 final int ALL_BUT_FIRST = 1;
398 StringBuilder result = new StringBuilder();
399 result.append( Character.toUpperCase(aText.charAt(FIRST)) );
400 result.append(aText.substring(ALL_BUT_FIRST));
401 return result.toString();
402 }
403
404 /**
405 Ensure <tt>aText</tt> contains no spaces.
406
407 <P>Along with {@link #withInitialCapital(String)}, this method is useful for
408 mapping request parameter names into corresponding <tt>getXXX</tt> methods.
409 For example, the text <tt>'Email Address'</tt> and <tt>'emailAddress'</tt>
410 can <em>both</em> be mapped to a method named <tt>'getEmailAddress()'</tt>, by using :
411 <PRE>
412 String methodName = "get" + Util.withNoSpaces(Util.withInitialCapital(name));
413 </PRE>
414
415 @param aText has content
416 */
417 public static String withNoSpaces(String aText){
418 Args.checkForContent(aText);
419 return replace(aText.trim(), Consts.SPACE, Consts.EMPTY_STRING);
420 }
421
422 /**
423 Replace every occurrence of a fixed substring with substitute text.
424
425 <P>This method is distinct from {@link String#replaceAll}, since it does not use a
426 regular expression.
427
428 @param aInput may contain substring <tt>aOldText</tt>; satisfies {@link #textHasContent(String)}
429 @param aOldText substring which is to be replaced; possibly empty, but never null
430 @param aNewText replacement for <tt>aOldText</tt>; possibly empty, but never null
431 */
432 public static String replace(String aInput, String aOldText, String aNewText){
433 if ( ! textHasContent(aInput) ) {
434 throw new IllegalArgumentException("Input must have content.");
435 }
436 if ( aNewText == null ) {
437 throw new NullPointerException("Replacement text may be empty, but never null.");
438 }
439 final StringBuilder result = new StringBuilder();
440 //startIdx and idxOld delimit various chunks of aInput; these
441 //chunks always end where aOldText begins
442 int startIdx = 0;
443 int idxOld = 0;
444 while ((idxOld = aInput.indexOf(aOldText, startIdx)) >= 0) {
445 //grab a part of aInput which does not include aOldPattern
446 result.append( aInput.substring(startIdx, idxOld) );
447 //add replacement text
448 result.append( aNewText );
449 //reset the startIdx to just after the current match, to see
450 //if there are any further matches
451 startIdx = idxOld + aOldText.length();
452 }
453 //the final chunk will go to the end of aInput
454 result.append( aInput.substring(startIdx) );
455 return result.toString();
456 }
457
458 /**
459 Return a <tt>String</tt> suitable for logging, having one item from <tt>aCollection</tt>
460 per line.
461
462 <P>For the <tt>Collection</tt> containing <br>
463 <tt>[null, "Zebra", "aardvark", "Banana", "", "aardvark", new BigDecimal("5.00")]</tt>,
464
465 <P>the return value is :
466 <PRE>
467 (7) {
468 ''
469 '5.00'
470 'aardvark'
471 'aardvark'
472 'Banana'
473 'null'
474 'Zebra'
475 }
476 </PRE>
477
478 <P>The text for each item is generated by calling {@link #quote}, and by appending a new line.
479
480 <P>As well, this method reports the total number of items, <em>and places items in
481 alphabetical order</em> (ignoring case). (The iteration order of the <tt>Collection</tt>
482 passed by the caller will often differ from the order of items presented in the return value.)
483 </PRE>
484 */
485 public static String logOnePerLine(Collection<?> aCollection){
486 int STANDARD_INDENTATION = 1;
487 return logOnePerLine(aCollection, STANDARD_INDENTATION);
488 }
489
490 /**
491 As in {@link #logOnePerLine(Collection)}, but with specified indentation level.
492
493 @param aIndentLevel greater than or equal to 1, acts as multiplier for a
494 "standard" indentation level of two spaces.
495 */
496 public static String logOnePerLine(Collection<?> aCollection, int aIndentLevel){
497 Args.checkForPositive(aIndentLevel);
498 String indent = getIndentation(aIndentLevel);
499 StringBuilder result = new StringBuilder();
500 result.append("(" + aCollection.size() + ") {" + Consts.NEW_LINE);
501 List<String> lines = new ArrayList<String>(aCollection.size());
502 for (Object item: aCollection){
503 StringBuilder line = new StringBuilder(indent);
504 line.append( quote(item) ); //nulls ok
505 line.append( Consts.NEW_LINE );
506 lines.add(line.toString());
507 }
508 addSortedLinesToResult(result, lines);
509 result.append(getFinalIndentation(aIndentLevel));
510 result.append("}");
511 return result.toString();
512 }
513
514 /**
515 Return a <tt>String</tt> suitable for logging, having one item from <tt>aMap</tt>
516 per line.
517
518 <P>For a <tt>Map</tt> containing <br>
519 <tt>["b"="blah", "a"=new BigDecimal(5.00), "Z"=null, null=new Integer(3)]</tt>,
520
521 <P>the return value is :
522 <PRE>
523 (4) {
524 'a' = '5.00'
525 'b' = 'blah'
526 'null' = '3'
527 'Z' = 'null'
528 }
529 </PRE>
530
531 <P>The text for each key and value is generated by calling {@link #quote}, and
532 appending a new line after each entry.
533
534 <P>As well, this method reports the total number of items, <em>and places items in
535 alphabetical order of their keys</em> (ignoring case). (The iteration order of the
536 <tt>Map</tt> passed by the caller will often differ from the order of items in the
537 return value.)
538
539 <P>An attempt is made to suppress the emission of passwords. Values in a Map are
540 presented as <tt>****</tt> if the following conditions are all true :
541 <ul>
542 <li>{@link String#valueOf(java.lang.Object)} applied to the <em>key</em> contains the word <tt>password</tt>
543 (ignoring case)
544 <li>the <em>value</em> is not an array or a <tt>Collection</tt>
545 </ul>
546 */
547 public static String logOnePerLine(Map<?,?> aMap){
548 StringBuilder result = new StringBuilder();
549 result.append("(" + aMap.size() + ") {" + Consts.NEW_LINE);
550 List<String> lines = new ArrayList<String>(aMap.size());
551 String SEPARATOR = " = ";
552 Iterator iter = aMap.keySet().iterator();
553 while ( iter.hasNext() ){
554 Object key = iter.next();
555 StringBuilder line = new StringBuilder(INDENT);
556 line.append(quote(key)); //nulls ok
557 line.append(SEPARATOR);
558 Object value = aMap.get(key);
559 int MORE_INDENTATION = 2;
560 if ( value != null && value instanceof Collection) {
561 line.append( logOnePerLine((Collection)value, MORE_INDENTATION));
562 }
563 else if ( value != null && value.getClass().isArray() ){
564 List valueItems = Arrays.asList( (Object[])value );
565 line.append( logOnePerLine(valueItems, MORE_INDENTATION) );
566 }
567 else {
568 value = suppressPasswords(key, value);
569 line.append(quote(value)); //nulls ok
570 }
571 line.append(Consts.NEW_LINE);
572 lines.add(line.toString());
573 }
574 addSortedLinesToResult(result, lines);
575 result.append("}");
576 return result.toString();
577 }
578
579 /**
580 Return a {@link Locale} object by parsing <tt>aRawLocale</tt>.
581
582 <P>The format of <tt>aRawLocale</tt> follows the
583 <tt>language_country_variant</tt> style used by {@link Locale}. The value is <i>not</i> checked against
584 {@link Locale#getAvailableLocales()}.
585 */
586 public static Locale buildLocale(String aRawLocale){
587 int language = 0;
588 int country = 1;
589 int variant = 2;
590 Locale result = null;
591 fLogger.finest("Raw Locale: " + aRawLocale);
592 String[] parts = aRawLocale.split("_");
593 if (parts.length == 1) {
594 result = new Locale( parts[language] );
595 }
596 else if (parts.length == 2) {
597 result = new Locale( parts[language], parts[country] );
598 }
599 else if (parts.length == 3 ) {
600 result = new Locale( parts[language], parts[country], parts[variant] );
601 }
602 else {
603 throw new AssertionError("Locale identifer has unexpected format: " + aRawLocale);
604 }
605 fLogger.finest("Parsed Locale : " + Util.quote(result.toString()));
606 return result;
607 }
608
609 /**
610 Return a {@link TimeZone} corresponding to a given {@link String}.
611
612 <P>If the given <tt>String</tt> does not correspond to a known <tt>TimeZone</tt> id,
613 as determined by {@link TimeZone#getAvailableIDs()}, then a
614 runtime exception is thrown. (This differs from the behavior of the
615 {@link TimeZone} class itself, and is the reason why this method exists.)
616 */
617 public static TimeZone buildTimeZone(String aTimeZone) {
618 TimeZone result = null;
619 List<String> timeZones = Arrays.asList(TimeZone.getAvailableIDs());
620 if( timeZones.contains(aTimeZone.trim()) ) {
621 result = TimeZone.getTimeZone(aTimeZone.trim());
622 }
623 else {
624 fLogger.severe("Unknown Time Zone : " + quote(aTimeZone));
625 //fLogger.severe("Known Time Zones : " + logOnePerLine(timeZones));
626 throw new IllegalArgumentException("Unknown TimeZone Id : " + quote(aTimeZone));
627 }
628 return result;
629 }
630
631 /**
632 Convenience method for producing a simple textual
633 representation of an array.
634
635 <P>The format of the returned {@link String} is the same as
636 {@link java.util.AbstractCollection#toString} :
637 <ul>
638 <li>non-empty array: <tt>[blah, blah]</tt>
639 <li>empty array: <tt>[]</tt>
640 <li>null array: <tt>null</tt>
641 </ul>
642
643 <P>Thanks to Jerome Lacoste for improving the implementation of this method.
644
645 @param aArray is a possibly-null array whose elements are
646 primitives or objects. Arrays of arrays are also valid, in which case
647 <tt>aArray</tt> is rendered in a nested, recursive fashion.
648
649 */
650 public static String getArrayAsString(Object aArray){
651 final String fSTART_CHAR = "[";
652 final String fEND_CHAR = "]";
653 final String fSEPARATOR = ", ";
654 final String fNULL = "null";
655
656 if ( aArray == null ) return fNULL;
657 checkObjectIsArray(aArray);
658
659 StringBuilder result = new StringBuilder( fSTART_CHAR );
660 int length = Array.getLength(aArray);
661 for ( int idx = 0 ; idx < length ; ++idx ) {
662 Object item = Array.get(aArray, idx);
663 if ( isNonNullArray(item) ){
664 //recursive call!
665 result.append( getArrayAsString(item) );
666 }
667 else{
668 result.append( item );
669 }
670 if ( ! isLastItem(idx, length) ) {
671 result.append(fSEPARATOR);
672 }
673 }
674 result.append(fEND_CHAR);
675 return result.toString();
676 }
677
678 /**
679 Transform a <tt>List</tt> into a <tt>Map</tt>.
680
681 <P>This method exists because it is sometimes necessary to transform a
682 <tt>List</tt> into a lookup table of some sort, using <em>unique</em> keys already
683 present in the <tt>List</tt> data.
684
685 <P>The <tt>List</tt> to be transformed contains objects having a method named
686 <tt>aKeyMethodName</tt>, and which returns objects of class <tt>aKeyClass</tt>.
687 Thus, data is extracted from each object to act as its key. Furthermore, the
688 key must be <em>unique</em>. If any duplicates are detected, then an
689 exception is thrown. This ensures that the returned <tt>Map</tt> will be the
690 same size as the given <tt>List</tt>, and that no data is silently discarded.
691
692 <P>The iteration order of the returned <tt>Map</tt> is identical to the iteration order of
693 the input <tt>List</tt>.
694 */
695 public static <K,V> Map<K,V> asMap(List<V> aList, Class<K> aClass, String aKeyMethodName){
696 Map<K,V> result = new LinkedHashMap<K,V>();
697 for(V value: aList){
698 K key = getMethodValue(value, aClass, aKeyMethodName);
699 if( result.containsKey(key) ){
700 throw new IllegalArgumentException("Key must be unique. Duplicate detected : " + quote(key));
701 }
702 result.put(key, value);
703 }
704 return result;
705 }
706
707 /**
708 Reverse the keys and values in a <tt>Map</tt>.
709
710 <P>This method exists because sometimes a lookup operation needs to be performed in a
711 style opposite to an existing <tt>Map</tt>.
712
713 <P>There is an unusual requirement on the <tt>Map</tt> argument: the map <em>values</em> must be
714 unique. Thus, the returned <tt>Map</tt> will be the same size as the input <tt>Map</tt>. If any
715 duplicates are detected, then an exception is thrown.
716
717 <P>The iteration order of the returned <tt>Map</tt> is identical to the iteration order of
718 the input <tt>Map</tt>.
719 */
720 public static <K,V> Map<V,K> reverseMap(Map<K,V> aMap){
721 Map<V,K> result = new LinkedHashMap<V,K>();
722 for(Map.Entry<K,V> entry: aMap.entrySet()){
723 if( result.containsKey(entry.getValue())){
724 throw new IllegalArgumentException("Value must be unique. Duplicate detected : " + quote(entry.getValue()));
725 }
726 result.put(entry.getValue(), entry.getKey());
727 }
728 return result;
729 }
730
731
732 // PRIVATE //
733
734 private Util(){
735 //empty - prevents construction by the caller.
736 }
737
738 private static final Logger fLogger = Util.getLogger(Util.class);
739 private static final String INDENT = Consts.SPACE + Consts.SPACE;
740 private static final Pattern PASSWORD = Pattern.compile("password", Pattern.CASE_INSENSITIVE);
741
742 private static void addSortedLinesToResult(StringBuilder aResult, List<String> aLines) {
743 Collections.sort(aLines, String.CASE_INSENSITIVE_ORDER);
744 for (String line: aLines){
745 aResult.append( line );
746 }
747 }
748
749 private static String getIndentation(int aIndentLevel){
750 StringBuilder result = new StringBuilder();
751 for (int idx = 1; idx <= aIndentLevel; ++idx){
752 result.append(INDENT);
753 }
754 return result.toString();
755 }
756
757 private static String getFinalIndentation(int aIndentLevel){
758 return getIndentation(aIndentLevel - 1);
759 }
760
761 /**
762 Replace likely password values with a fixed string.
763 */
764 private static Object suppressPasswords(Object aKey, Object aValue){
765 Object result = aValue;
766 String key = String.valueOf(aKey);
767 Matcher matcher = PASSWORD.matcher(key);
768 if ( matcher.find() ){
769 result = "*****";
770 }
771 return result;
772 }
773
774 private static void checkObjectIsArray(Object aArray){
775 if ( ! aArray.getClass().isArray() ) {
776 throw new IllegalArgumentException("Object is not an array.");
777 }
778 }
779
780 private static boolean isNonNullArray(Object aItem){
781 return aItem != null && aItem.getClass().isArray();
782 }
783
784 private static boolean isLastItem(int aIdx, int aLength){
785 return (aIdx == aLength - 1);
786 }
787
788 private static <K> K getMethodValue(Object aValue, Class<K> aClass, String aKeyMethodName){
789 K result = null;
790 try {
791 Method method = aValue.getClass().getMethod(aKeyMethodName); //no args
792 result = (K)method.invoke(aValue);
793 }
794 catch (NoSuchMethodException ex){
795 handleInvocationEx(aValue.getClass(), aKeyMethodName);
796 }
797 catch (IllegalAccessException ex){
798 handleInvocationEx(aValue.getClass(), aKeyMethodName);
799 }
800 catch (InvocationTargetException ex){
801 handleInvocationEx(aValue.getClass(), aKeyMethodName);
802 }
803 return result;
804 }
805
806 private static void handleInvocationEx(Class<?> aClass, String aKeyMethodName){
807 throw new IllegalArgumentException("Cannot invoke method named " + quote(aKeyMethodName) + " on object of class " + quote(aClass));
808 }
809 }