View Javadoc

1   package org.apache.velocity.tools;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.lang.reflect.Array;
24  import java.net.URL;
25  import java.text.DateFormat;
26  import java.text.DecimalFormat;
27  import java.text.DecimalFormatSymbols;
28  import java.text.NumberFormat;
29  import java.text.SimpleDateFormat;
30  import java.util.Collection;
31  import java.util.Date;
32  import java.util.Calendar;
33  import java.util.Locale;
34  import java.util.TimeZone;
35  import java.util.concurrent.ConcurrentMap;
36  import java.util.concurrent.ConcurrentHashMap;
37  
38  /**
39   * Utility methods for parsing or otherwise converting between types.
40   * Current supported types are Number, Date, Calendar, 
41   * String, Boolean, Locale and URL
42   *
43   * @author Nathan Bubna
44   */
45  public class ConversionUtils
46  {
47      public static final ConversionUtils INSTANCE = new ConversionUtils();
48  
49      private static final int STYLE_NUMBER       = 0;
50      private static final int STYLE_CURRENCY     = 1;
51      private static final int STYLE_PERCENT      = 2;
52      //NOTE: '3' belongs to a non-public "scientific" style
53      private static final int STYLE_INTEGER      = 4;
54  
55      // cache custom formats
56      private static ConcurrentMap<String,NumberFormat> customFormatsCache = new ConcurrentHashMap<String,NumberFormat>();
57  
58      private ConversionUtils() {}
59  
60      public ConversionUtils getInstance()
61      {
62          return INSTANCE;
63      }
64  
65  
66      /**
67       * Returns a {@link NumberFormat} instance for the specified
68       * format and {@link Locale}.  If the format specified is a standard
69       * style pattern, then a number instance
70       * will be returned with the number style set to the
71       * specified style.  If it is a custom format, then a customized
72       * {@link NumberFormat} will be returned.
73       *
74       * @param format the custom or standard formatting pattern to be used
75       * @param locale the {@link Locale} to be used
76       * @return an instance of {@link NumberFormat}
77       * @see NumberFormat
78       */
79      public static NumberFormat getNumberFormat(String format, Locale locale)
80      {
81          if (format == null || locale == null)
82          {
83              return null;
84          }
85  
86          NumberFormat nf = null;
87          int style = getNumberStyleAsInt(format);
88          if (style < 0)
89          {
90              // we have a custom format
91              String cacheKey = format + "%" + locale.toString();
92              nf = customFormatsCache.get(cacheKey);
93              if( nf == null )
94              {
95                  nf = new DecimalFormat(format, new DecimalFormatSymbols(locale));
96                  customFormatsCache.put(cacheKey,nf);
97              }
98          }
99          else
100         {
101             // we have a standard format
102             nf = getNumberFormat(style, locale);
103         }
104         return nf;
105     }
106 
107     /**
108      * Returns a {@link NumberFormat} instance for the specified
109      * number style and {@link Locale}.
110      *
111      * @param numberStyle the number style (number will be ignored if this is
112      *        less than zero or the number style is not recognized)
113      * @param locale the {@link Locale} to be used
114      * @return an instance of {@link NumberFormat} or <code>null</code>
115      *         if an instance cannot be constructed with the given
116      *         parameters
117      */
118     public static NumberFormat getNumberFormat(int numberStyle, Locale locale)
119     {
120         try
121         {
122             NumberFormat nf;
123             switch (numberStyle)
124             {
125                 case STYLE_NUMBER:
126                     nf = NumberFormat.getNumberInstance(locale);
127                     break;
128                 case STYLE_CURRENCY:
129                     nf = NumberFormat.getCurrencyInstance(locale);
130                     break;
131                 case STYLE_PERCENT:
132                     nf = NumberFormat.getPercentInstance(locale);
133                     break;
134                 case STYLE_INTEGER:
135                     nf = NumberFormat.getIntegerInstance(locale);
136                     break;
137                 default:
138                     // invalid style was specified, return null
139                     nf = null;
140             }
141             return nf;
142         }
143         catch (Exception suppressed)
144         {
145             // let it go...
146             return null;
147         }
148     }
149 
150     /**
151      * Checks a string to see if it matches one of the standard
152      * NumberFormat style patterns:
153      *      number, currency, percent, integer, or default.
154      * if it does it will return the integer constant for that pattern.
155      * if not, it will return -1.
156      *
157      * @see NumberFormat
158      * @param style the string to be checked
159      * @return the int identifying the style pattern
160      */
161     public static int getNumberStyleAsInt(String style)
162     {
163         // avoid needlessly running through all the string comparisons
164         if (style == null || style.length() < 6 || style.length() > 8) {
165             return -1;
166         }
167         if (style.equalsIgnoreCase("default"))
168         {
169             //NOTE: java.text.NumberFormat returns "number" instances
170             //      as the default (at least in Java 1.3 and 1.4).
171             return STYLE_NUMBER;
172         }
173         if (style.equalsIgnoreCase("number"))
174         {
175             return STYLE_NUMBER;
176         }
177         if (style.equalsIgnoreCase("currency"))
178         {
179             return STYLE_CURRENCY;
180         }
181         if (style.equalsIgnoreCase("percent"))
182         {
183             return STYLE_PERCENT;
184         }
185         if (style.equalsIgnoreCase("integer"))
186         {
187             return STYLE_INTEGER;
188         }
189         // ok, it's not any of the standard patterns
190         return -1;
191     }
192 
193 
194     // ----------------- number conversion methods ---------------
195 
196     /**
197      * Attempts to convert an unidentified {@link Object} into a {@link Number},
198      * just short of turning it into a string and parsing it.  In other words,
199      * this will convert to {@link Number} from a {@link Number}, {@link Calendar},
200      * or {@link Date}.  If it can't do that, it will get the string value and have 
201      * {@link #toNumber(String,String,Locale)} try to parse it using the
202      * default Locale and format.
203      
204      * @param obj - the object to convert
205      */
206     public static Number toNumber(Object obj)
207     {
208         return toNumber(obj, true);
209     }
210 
211     /**
212      * Just like {@link #toNumber(Object)} except that you can tell
213      * this to attempt parsing the object as a String by passing {@code true}
214      * as the second parameter.  If you do so, then it will have
215      * {@link #toNumber(String,String,Locale)} try to parse it using the
216      * default Locale and format.
217      */
218     public static Number toNumber(Object obj, boolean handleStrings)
219     {
220         if (obj == null)
221         {
222             return null;
223         }
224         if (obj instanceof Number)
225         {
226             return (Number)obj;
227         }
228         if (obj instanceof Date)
229         {
230             return Long.valueOf(((Date)obj).getTime());
231         }
232         if (obj instanceof Calendar)
233         {
234             Date date = ((Calendar)obj).getTime();
235             return Long.valueOf(date.getTime());
236         }
237         if (handleStrings)
238         {
239             // try parsing with default format and locale
240             return toNumber(obj.toString(), "default", Locale.getDefault());
241         }
242         return null;
243     }
244 
245     /**
246      * Converts a string to an instance of {@link Number} using the
247      * specified format and {@link Locale} to parse it.
248      *
249      * @param value - the string to convert
250      * @param format - the format the number is in
251      * @param locale - the {@link Locale}
252      * @return the string as a {@link Number} or <code>null</code> if no
253      *         conversion is possible
254      * @see NumberFormat#parse
255      */
256     public static Number toNumber(String value, String format, Locale locale)
257     {
258         if (value == null || format == null || locale == null)
259         {
260             return null;
261         }
262         try
263         {
264             NumberFormat parser = getNumberFormat(format, locale);
265             return parser.parse(value);
266         }
267         catch (Exception e)
268         {
269             return null;
270         }
271     }
272 
273     /**
274      * Converts an object to an instance of {@link Number} using the
275      * specified format and {@link Locale} to parse it, if necessary.
276      *
277      * @param value - the object to convert
278      * @param format - the format the number is in
279      * @param locale - the {@link Locale}
280      * @return the object as a {@link Number} or <code>null</code> if no
281      *         conversion is possible
282      * @see NumberFormat#parse
283      */
284     public static Number toNumber(Object value, String format, Locale locale)
285     {
286         // first try the easy stuff
287         Number number = toNumber(value, false);
288         if (number != null)
289         {
290             return number;
291         }
292 
293         // turn it into a string and try parsing it
294         return toNumber(String.valueOf(value), format, locale);
295     }
296 
297 
298     // -------------------------- DateFormat creation methods --------------
299 
300     /**
301      * Returns a {@link DateFormat} instance for the specified
302      * format, {@link Locale}, and {@link TimeZone}.  If the format
303      * specified is a standard style pattern, then a date-time instance
304      * will be returned with both the date and time styles set to the
305      * specified style.  If it is a custom format, then a customized
306      * {@link SimpleDateFormat} will be returned.
307      *
308      * @param format the custom or standard formatting pattern to be used
309      * @param locale the {@link Locale} to be used
310      * @param timezone the {@link TimeZone} to be used
311      * @return an instance of {@link DateFormat}
312      * @see SimpleDateFormat
313      * @see DateFormat
314      */
315     public static DateFormat getDateFormat(String format, Locale locale,
316                                            TimeZone timezone)
317     {
318         if (format == null)
319         {
320             return null;
321         }
322 
323         DateFormat df = null;
324         // do they want a date instance
325         if (format.endsWith("_date"))
326         {
327             String fmt = format.substring(0, format.length() - 5);
328             int style = getDateStyleAsInt(fmt);
329             df = getDateFormat(style, -1, locale, timezone);
330         }
331         // do they want a time instance?
332         else if (format.endsWith("_time"))
333         {
334             String fmt = format.substring(0, format.length() - 5);
335             int style = getDateStyleAsInt(fmt);
336             df = getDateFormat(-1, style, locale, timezone);
337         }
338         // ok, they either want a custom or date-time instance
339         else
340         {
341             int style = getDateStyleAsInt(format);
342             if (style < 0)
343             {
344                 // we have a custom format
345                 df = new SimpleDateFormat(format, locale);
346                 df.setTimeZone(timezone);
347             }
348             else
349             {
350                 // they want a date-time instance
351                 df = getDateFormat(style, style, locale, timezone);
352             }
353         }
354         return df;
355     }
356 
357     /**
358      * Returns a {@link DateFormat} instance for the specified
359      * date style, time style, {@link Locale}, and {@link TimeZone}.
360      *
361      * @param dateStyle the date style
362      * @param timeStyle the time style
363      * @param locale the {@link Locale} to be used
364      * @param timezone the {@link TimeZone} to be used
365      * @return an instance of {@link DateFormat}
366      * @see #getDateFormat(int timeStyle, int dateStyle, Locale locale, TimeZone timezone)
367      */
368     public static DateFormat getDateFormat(String dateStyle, String timeStyle,
369                                            Locale locale, TimeZone timezone)
370     {
371         int ds = getDateStyleAsInt(dateStyle);
372         int ts = getDateStyleAsInt(timeStyle);
373         return getDateFormat(ds, ts, locale, timezone);
374     }
375 
376     /**
377      * Returns a {@link DateFormat} instance for the specified
378      * time style, date style, {@link Locale}, and {@link TimeZone}.
379      *
380      * @param dateStyle the date style (date will be ignored if this is
381      *        less than zero and the date style is not)
382      * @param timeStyle the time style (time will be ignored if this is
383      *        less than zero and the date style is not)
384      * @param locale the {@link Locale} to be used
385      * @param timezone the {@link TimeZone} to be used
386      * @return an instance of {@link DateFormat} or <code>null</code>
387      *         if an instance cannot be constructed with the given
388      *         parameters
389      */
390     public static DateFormat getDateFormat(int dateStyle, int timeStyle,
391                                            Locale locale, TimeZone timezone)
392     {
393         try
394         {
395             DateFormat df;
396             if (dateStyle < 0 && timeStyle < 0)
397             {
398                 // no style was specified, use default instance
399                 df = DateFormat.getInstance();
400             }
401             else if (timeStyle < 0)
402             {
403                 // only a date style was specified
404                 df = DateFormat.getDateInstance(dateStyle, locale);
405             }
406             else if (dateStyle < 0)
407             {
408                 // only a time style was specified
409                 df = DateFormat.getTimeInstance(timeStyle, locale);
410             }
411             else
412             {
413                 df = DateFormat.getDateTimeInstance(dateStyle, timeStyle,
414                                                     locale);
415             }
416             df.setTimeZone(timezone);
417             return df;
418         }
419         catch (Exception suppressed)
420         {
421             // let it go...
422             return null;
423         }
424     }
425 
426     /**
427      * Checks a string to see if it matches one of the standard DateFormat
428      * style patterns: full, long, medium, short, or default.  If it does,
429      * it will return the integer constant for that pattern.  If not, it
430      * will return -1.
431      *
432      * @see DateFormat
433      * @param style the string to be checked
434      * @return the int identifying the style pattern
435      */
436     public static int getDateStyleAsInt(String style)
437     {
438         // avoid needlessly running through all the string comparisons
439         if (style == null || style.length() < 4 || style.length() > 7) {
440             return -1;
441         }
442         if (style.equalsIgnoreCase("full"))
443         {
444             return DateFormat.FULL;
445         }
446         if (style.equalsIgnoreCase("long"))
447         {
448             return DateFormat.LONG;
449         }
450         if (style.equalsIgnoreCase("medium"))
451         {
452             return DateFormat.MEDIUM;
453         }
454         if (style.equalsIgnoreCase("short"))
455         {
456             return DateFormat.SHORT;
457         }
458         if (style.equalsIgnoreCase("default"))
459         {
460             return DateFormat.DEFAULT;
461         }
462         // ok, it's not any of the standard patterns
463         return -1;
464     }
465 
466 
467     // ----------------- date conversion methods ---------------
468 
469     /**
470      * Attempts to convert an unidentified {@link Object} into a {@link Date},
471      * just short of turning it into a string and parsing it.  In other words,
472      * this will convert to {@link Date} from a {@link Date}, {@link Calendar},
473      * or {@link Number}.  If it can't do that, it will return {@code null}.
474      *
475      * @param obj - the object to convert
476      */
477     public static Date toDate(Object obj)
478     {
479         if (obj == null)
480         {
481             return null;
482         }
483         if (obj instanceof Date)
484         {
485             return (Date)obj;
486         }
487         if (obj instanceof Calendar)
488         {
489             return ((Calendar)obj).getTime();
490         }
491         if (obj instanceof Number)
492         {
493             Date d = new Date();
494             d.setTime(((Number)obj).longValue());
495             return d;
496         }
497         return null;
498     }
499 
500     /**
501      * Converts an object to an instance of {@link Date} using the
502      * specified format, {@link Locale}, and {@link TimeZone} if the
503      * object is not already an instance of Date, Calendar, or Long.
504      *
505      * @param obj - the date to convert
506      * @param format - the format the date is in
507      * @param locale - the {@link Locale}
508      * @param timezone - the {@link TimeZone}
509      * @return the object as a {@link Date} or <code>null</code> if no
510      *         conversion is possible
511      * @see #getDateFormat
512      * @see SimpleDateFormat#parse
513      */
514     public static Date toDate(Object obj, String format,
515                               Locale locale, TimeZone timezone)
516     {
517         // first try the easy stuff
518         Date date = toDate(obj);
519         if (date != null)
520         {
521             return date;
522         }
523 
524         // turn it into a string and try parsing it
525         return toDate(String.valueOf(obj), format, locale, timezone);
526     }
527 
528     /**
529      * Converts an object to an instance of {@link Date} using the
530      * specified format, {@link Locale}, and {@link TimeZone} if the
531      * object is not already an instance of Date, Calendar, or Long.
532      *
533      * @param str - the string to parse
534      * @param format - the format the date is in
535      * @param locale - the {@link Locale}
536      * @param timezone - the {@link TimeZone}
537      * @return the string as a {@link Date} or <code>null</code> if the
538      *         parsing fails
539      * @see #getDateFormat
540      * @see SimpleDateFormat#parse
541      */
542     public static Date toDate(String str, String format,
543                               Locale locale, TimeZone timezone)
544     {
545         try
546         {
547             //try parsing w/a customized SimpleDateFormat
548             DateFormat parser = getDateFormat(format, locale, timezone);
549             return parser.parse(str);
550         }
551         catch (Exception e)
552         {
553             return null;
554         }
555     }
556 
557     public static Calendar toCalendar(Date date, Locale locale)
558     {
559         if (date == null)
560         {
561             return null;
562         }
563 
564         Calendar cal;
565         if (locale == null)
566         {
567             cal = Calendar.getInstance();
568         }
569         else
570         {
571             cal = Calendar.getInstance(locale);
572         }
573         cal.setTime(date);
574         // HACK: Force all fields to update. see link for explanation of this.
575         //http://java.sun.com/j2se/1.4/docs/api/java/util/Calendar.html
576         cal.getTime();
577         return cal;
578     }
579 
580 
581     // ----------------- misc conversion methods ---------------
582 
583     /**
584      * Converts objects to String in a more Tools-ish way than
585      * String.valueOf(Object), especially with nulls, Arrays and Collections.
586      * Null returns null, Arrays and Collections return their first value,
587      * or null if they have no values.
588      * 
589      * @param value the object to be turned into a String
590      * @return the string value of the object or null if the value is null
591      *         or it is an array whose first value is null
592      */
593     public static String toString(Object value)
594     {
595         if (value instanceof String)
596         {
597             return (String)value;
598         }
599         if (value == null)
600         {
601             return null;
602         }
603         if (value.getClass().isArray())
604         {
605             if (Array.getLength(value) > 0)
606             {
607                 // recurse on the first value
608                 return toString(Array.get(value, 0));
609             }
610             return null;
611         }
612         return String.valueOf(value);
613     }
614 
615     /**
616      * Returns the first value as a String, if any; otherwise returns null.
617      *
618      * @param values the Collection to be turned into a string
619      * @return the string value of the first object in the collection
620      *         or null if the collection is empty
621      */
622     public static String toString(Collection values)
623     {
624         if (values != null && !values.isEmpty())
625         {
626             // recurse on the first value
627             return toString(values.iterator().next());
628         }
629         return null;
630     }
631 
632     /**
633      * Converts any Object to a boolean using {@link #toString(Object)}
634      * and {@link Boolean#valueOf(String)}. 
635      *
636      * @param value the object to be converted
637      * @return a {@link Boolean} object for the specified value or
638      *         <code>null</code> if the value is null or the conversion failed
639      */
640     public static Boolean toBoolean(Object value)
641     {
642         if (value instanceof Boolean)
643         {
644             return (Boolean)value;
645         }
646 
647         String s = toString(value);
648         return (s != null) ? Boolean.valueOf(s) : null;
649     }
650 
651     /**
652      * Converts a string to a {@link Locale}
653      *
654      * @param value - the string to parse
655      * @return the {@link Locale} or <code>null</code> if the
656      *         parsing fails
657      */
658     public static Locale toLocale(String value)
659     {
660         if (value.indexOf('_') < 0)
661         {
662             return new Locale(value);
663         }
664 
665         String[] params = value.split("_");
666         if (params.length == 2)
667         {
668             return new Locale(params[0], params[1]);
669         }
670         else if (params.length == 3)
671         {
672             return new Locale(params[0], params[1], params[2]);
673         }
674         else
675         {
676             // there's only 3 possible params, so this must be invalid
677             return null;
678         }
679     }
680 
681     /**
682      * Converts a string to a {@link URL}.  It will first try to
683      * treat the string as a File name, then a classpath resource,
684      * then finally as a literal URL.  If none of these work, then
685      * this will return {@code null}.
686      *
687      * @param value - the string to parse
688      * @return the {@link URL} form of the string or {@code null}
689      * @see File
690      * @see ClassUtils#getResource(String,Object)
691      * @see URL
692      */
693     public static URL toURL(String value)
694     {
695         return toURL(value, ConversionUtils.class);
696     }
697 
698     /**
699      * Converts a string to a {@link URL}.  It will first try to
700      * treat the string as a File name, then a classpath resource,
701      * then finally as a literal URL.  If none of these work, then
702      * this will return {@code null}.
703      *
704      * @param value - the string to parse
705      * @param caller - the object or Class seeking the url
706      * @return the {@link URL} form of the string or {@code null}
707      * @see File
708      * @see ClassUtils#getResource(String,Object)
709      * @see URL
710      */
711     public static URL toURL(String value, Object caller)
712     {
713         try
714         {
715             File file = new File(value);
716             if (file.exists())
717             {
718                 return file.toURI().toURL();
719             }
720         }
721         catch (Exception e) {}
722         try
723         {
724             URL url = ClassUtils.getResource(value, caller);
725             if (url != null)
726             {
727                 return url;
728             }
729         }
730         catch (Exception e) {}
731         try
732         {
733             return new URL(value);
734         }
735         catch (Exception e) {}
736         return null;
737     }
738 
739 }