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    }