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    }