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 }