blob: da693f31d3c504b5195978d480410f0884c394b0 [file] [log] [blame]
package org.apache.velocity.tools;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.io.File;
import java.lang.reflect.Array;
import java.net.URL;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;
/**
* Utility methods for parsing or otherwise converting between types.
* Current supported types are Number, Date, Calendar,
* String, Boolean, Locale and URL
*
* @author Nathan Bubna
*/
public class ConversionUtils
{
public static final ConversionUtils INSTANCE = new ConversionUtils();
private static final int STYLE_NUMBER = 0;
private static final int STYLE_CURRENCY = 1;
private static final int STYLE_PERCENT = 2;
//NOTE: '3' belongs to a non-public "scientific" style
private static final int STYLE_INTEGER = 4;
/* Java DateFormat standard constants extensions */
private static final int STYLE_INTL = 5; /* international format without time zone*/
private static final int STYLE_ISO = 6; /* international format with timezone (RFC 3339) */
/* iso/intl date/time formats (locale-independant) */
private static DateFormat isoDateFormat = new SimpleDateFormat("yyyy-MM-dd"); /* date for iso/intl */
private static DateFormat intlTimeFormat = new SimpleDateFormat("HH:mm:ss"); /* time without timezone */
private static DateFormat isoTimeFormat = new SimpleDateFormat("HH:mm:ssXXX"); /* time with ISO 8601 timezone */
private static DateFormat intlTimestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); /* timestamp without timezone */
private static DateFormat isoTimestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssXXX"); /* timestamp with ISO 88601 timezone */
// cache custom formats
private static ConcurrentMap<String,NumberFormat> customFormatsCache = new ConcurrentHashMap<String,NumberFormat>();
private ConversionUtils() {}
public ConversionUtils getInstance()
{
return INSTANCE;
}
/**
* Returns a {@link NumberFormat} instance for the specified
* format and {@link Locale}. If the format specified is a standard
* style pattern, then a number instance
* will be returned with the number style set to the
* specified style. If it is a custom format, then a customized
* {@link NumberFormat} will be returned.
*
* @param format the custom or standard formatting pattern to be used
* @param locale the {@link Locale} to be used
* @return an instance of {@link NumberFormat}
* @see NumberFormat
*/
public static NumberFormat getNumberFormat(String format, Locale locale)
{
if (format == null || locale == null)
{
return null;
}
NumberFormat nf = null;
int style = getNumberStyleAsInt(format);
if (style < 0)
{
// we have a custom format
String cacheKey = format + "%" + locale.toString();
nf = customFormatsCache.get(cacheKey);
if( nf == null )
{
nf = new DecimalFormat(format, new DecimalFormatSymbols(locale));
customFormatsCache.put(cacheKey,nf);
}
}
else
{
// we have a standard format
nf = getNumberFormat(style, locale);
}
return nf;
}
/**
* Returns a {@link NumberFormat} instance for the specified
* number style and {@link Locale}.
*
* @param numberStyle the number style (number will be ignored if this is
* less than zero or the number style is not recognized)
* @param locale the {@link Locale} to be used
* @return an instance of {@link NumberFormat} or <code>null</code>
* if an instance cannot be constructed with the given
* parameters
*/
public static NumberFormat getNumberFormat(int numberStyle, Locale locale)
{
try
{
NumberFormat nf;
switch (numberStyle)
{
case STYLE_NUMBER:
nf = NumberFormat.getNumberInstance(locale);
break;
case STYLE_CURRENCY:
nf = NumberFormat.getCurrencyInstance(locale);
break;
case STYLE_PERCENT:
nf = NumberFormat.getPercentInstance(locale);
break;
case STYLE_INTEGER:
nf = NumberFormat.getIntegerInstance(locale);
break;
default:
// invalid style was specified, return null
nf = null;
}
return nf;
}
catch (Exception suppressed)
{
// let it go...
return null;
}
}
/**
* Checks a string to see if it matches one of the standard
* NumberFormat style patterns:
* number, currency, percent, integer, or default.
* if it does it will return the integer constant for that pattern.
* if not, it will return -1.
*
* @see NumberFormat
* @param style the string to be checked
* @return the int identifying the style pattern
*/
public static int getNumberStyleAsInt(String style)
{
// avoid needlessly running through all the string comparisons
if (style == null || style.length() < 6 || style.length() > 8) {
return -1;
}
if (style.equalsIgnoreCase("default"))
{
//NOTE: java.text.NumberFormat returns "number" instances
// as the default (at least in Java 1.3 and 1.4).
return STYLE_NUMBER;
}
if (style.equalsIgnoreCase("number"))
{
return STYLE_NUMBER;
}
if (style.equalsIgnoreCase("currency"))
{
return STYLE_CURRENCY;
}
if (style.equalsIgnoreCase("percent"))
{
return STYLE_PERCENT;
}
if (style.equalsIgnoreCase("integer"))
{
return STYLE_INTEGER;
}
// ok, it's not any of the standard patterns
return -1;
}
// ----------------- number conversion methods ---------------
/**
* Attempts to convert an unidentified {@link Object} into a {@link Number},
* just short of turning it into a string and parsing it. In other words,
* this will convert to {@link Number} from a {@link Number}, {@link Calendar},
* or {@link Date}. If it can't do that, it will get the string value and have
* {@link #toNumber(String,String,Locale)} try to parse it using the
* default Locale and format.
* @param obj - the object to convert
*/
public static Number toNumber(Object obj)
{
return toNumber(obj, true);
}
/**
* Just like {@link #toNumber(Object)} except that you can tell
* this to attempt parsing the object as a String by passing {@code true}
* as the second parameter. If you do so, then it will have
* {@link #toNumber(String,String,Locale)} try to parse it using the
* default Locale and format.
*/
public static Number toNumber(Object obj, boolean handleStrings)
{
if (obj == null)
{
return null;
}
if (obj instanceof Number)
{
return (Number)obj;
}
if (obj instanceof Date)
{
return Long.valueOf(((Date)obj).getTime());
}
if (obj instanceof Calendar)
{
Date date = ((Calendar)obj).getTime();
return Long.valueOf(date.getTime());
}
if (handleStrings)
{
// try parsing with default format and locale
return toNumber(obj.toString(), "default", Locale.getDefault());
}
return null;
}
/**
* Converts a string to an instance of {@link Number} using the
* specified format and {@link Locale} to parse it.
*
* @param value - the string to convert
* @param format - the format the number is in
* @param locale - the {@link Locale}
* @return the string as a {@link Number} or <code>null</code> if no
* conversion is possible
* @see NumberFormat#parse
*/
public static Number toNumber(String value, String format, Locale locale)
{
if (value == null || format == null || locale == null)
{
return null;
}
try
{
NumberFormat parser = getNumberFormat(format, locale);
return parser.parse(value);
}
catch (Exception e)
{
return null;
}
}
/**
* Converts an object to an instance of {@link Number} using the
* specified format and {@link Locale} to parse it, if necessary.
*
* @param value - the object to convert
* @param format - the format the number is in
* @param locale - the {@link Locale}
* @return the object as a {@link Number} or <code>null</code> if no
* conversion is possible
* @see NumberFormat#parse
*/
public static Number toNumber(Object value, String format, Locale locale)
{
// first try the easy stuff
Number number = toNumber(value, false);
if (number != null)
{
return number;
}
// turn it into a string and try parsing it
return toNumber(String.valueOf(value), format, locale);
}
// -------------------------- DateFormat creation methods --------------
/**
* Returns a {@link DateFormat} instance for the specified
* format, {@link Locale}, and {@link TimeZone}. If the format
* specified is a standard style pattern, then a date-time instance
* will be returned with both the date and time styles set to the
* specified style. If it is a custom format, then a customized
* {@link SimpleDateFormat} will be returned.
*
* @param format the custom or standard formatting pattern to be used
* @param locale the {@link Locale} to be used
* @param timezone the {@link TimeZone} to be used
* @return an instance of {@link DateFormat}
* @see SimpleDateFormat
* @see DateFormat
*/
public static DateFormat getDateFormat(String format, Locale locale,
TimeZone timezone)
{
if (format == null)
{
return null;
}
DateFormat df = null;
// do they want a date instance
if (format.endsWith("_date"))
{
String fmt = format.substring(0, format.length() - 5);
int style = getDateStyleAsInt(fmt);
df = getDateFormat(style, -1, locale, timezone);
}
// do they want a time instance?
else if (format.endsWith("_time"))
{
String fmt = format.substring(0, format.length() - 5);
int style = getDateStyleAsInt(fmt);
df = getDateFormat(-1, style, locale, timezone);
}
// ok, they either want a custom or date-time instance
else
{
int style = getDateStyleAsInt(format);
if (style < 0)
{
// we have a custom format
df = new SimpleDateFormat(format, locale);
df.setTimeZone(timezone);
}
else
{
// they want a date-time instance
df = getDateFormat(style, style, locale, timezone);
}
}
return df;
}
/**
* Returns a {@link DateFormat} instance for the specified
* date style, time style, {@link Locale}, and {@link TimeZone}.
*
* @param dateStyle the date style
* @param timeStyle the time style
* @param locale the {@link Locale} to be used
* @param timezone the {@link TimeZone} to be used
* @return an instance of {@link DateFormat}
* @see #getDateFormat(int timeStyle, int dateStyle, Locale locale, TimeZone timezone)
*/
public static DateFormat getDateFormat(String dateStyle, String timeStyle,
Locale locale, TimeZone timezone)
{
int ds = getDateStyleAsInt(dateStyle);
int ts = getDateStyleAsInt(timeStyle);
return getDateFormat(ds, ts, locale, timezone);
}
/**
* Returns a {@link DateFormat} instance for the specified
* time style, date style, {@link Locale}, and {@link TimeZone}.
*
* @param dateStyle the date style (date will be ignored if this is
* less than zero and the date style is not)
* @param timeStyle the time style (time will be ignored if this is
* less than zero and the date style is not)
* @param locale the {@link Locale} to be used
* @param timezone the {@link TimeZone} to be used
* @return an instance of {@link DateFormat} or <code>null</code>
* if an instance cannot be constructed with the given
* parameters
*/
public static DateFormat getDateFormat(int dateStyle, int timeStyle,
Locale locale, TimeZone timezone)
{
try
{
DateFormat df;
if (dateStyle < 0 && timeStyle < 0)
{
// no style was specified, use default instance
df = DateFormat.getInstance();
}
else if (timeStyle < 0)
{
// only a date style was specified
switch (dateStyle)
{
case STYLE_INTL:
case STYLE_ISO:
df = isoDateFormat;
break;
default:
df = DateFormat.getDateInstance(dateStyle, locale);
df.setTimeZone(timezone);
break;
}
}
else if (dateStyle < 0)
{
// only a time style was specified
switch (timeStyle)
{
case STYLE_INTL:
df = intlTimeFormat;
break;
case STYLE_ISO:
df = (DateFormat) isoTimeFormat.clone();
df.setTimeZone(timezone);
break;
default:
df = DateFormat.getTimeInstance(timeStyle, locale);
df.setTimeZone(timezone);
break;
}
}
else
{
switch (dateStyle)
{
case STYLE_INTL:
df = intlTimestampFormat;
break;
case STYLE_ISO:
df = (DateFormat) isoTimestampFormat.clone();
df.setTimeZone(timezone);
break;
default:
df = DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale);
df.setTimeZone(timezone);
break;
}
}
return df;
}
catch (Exception suppressed)
{
// let it go...
return null;
}
}
static Map<String, Integer> stylesMap;
static
{
stylesMap = new HashMap<String, Integer>();
stylesMap.put("FULL", DateFormat.FULL);
stylesMap.put("LONG", DateFormat.LONG);
stylesMap.put("MEDIUM", DateFormat.MEDIUM);
stylesMap.put("SHORT", DateFormat.SHORT);
stylesMap.put("DEFAULT", DateFormat.DEFAULT);
stylesMap.put("INTL", STYLE_INTL);
stylesMap.put("ISO", STYLE_ISO);
}
/**
* Checks a string to see if it matches one of the standard DateFormat
* style patterns: full, long, medium, short, or default. If it does,
* it will return the integer constant for that pattern. If not, it
* will return -1.
*
* @see DateFormat
* @param style the string to be checked
* @return the int identifying the style pattern
*/
public static int getDateStyleAsInt(String style)
{
Integer intStyle = stylesMap.get(style.toUpperCase());
return intStyle == null ? -1 : intStyle.intValue();
}
// ----------------- date conversion methods ---------------
/**
* Attempts to convert an unidentified {@link Object} into a {@link Date},
* just short of turning it into a string and parsing it. In other words,
* this will convert to {@link Date} from a {@link Date}, {@link Calendar},
* or {@link Number}. If it can't do that, it will return {@code null}.
*
* @param obj - the object to convert
*/
public static Date toDate(Object obj)
{
if (obj == null)
{
return null;
}
if (obj instanceof Date)
{
return (Date)obj;
}
if (obj instanceof Calendar)
{
return ((Calendar)obj).getTime();
}
if (obj instanceof Number)
{
Date d = new Date();
d.setTime(((Number)obj).longValue());
return d;
}
return null;
}
/**
* Converts an object to an instance of {@link Date} using the
* specified format, {@link Locale}, and {@link TimeZone} if the
* object is not already an instance of Date, Calendar, or Long.
*
* @param obj - the date to convert
* @param format - the format the date is in
* @param locale - the {@link Locale}
* @param timezone - the {@link TimeZone}
* @return the object as a {@link Date} or <code>null</code> if no
* conversion is possible
* @see #getDateFormat
* @see SimpleDateFormat#parse
*/
public static Date toDate(Object obj, String format,
Locale locale, TimeZone timezone)
{
// first try the easy stuff
Date date = toDate(obj);
if (date != null)
{
return date;
}
// turn it into a string and try parsing it
return toDate(String.valueOf(obj), format, locale, timezone);
}
/**
* Converts an object to an instance of {@link Date} using the
* specified format, {@link Locale}, and {@link TimeZone} if the
* object is not already an instance of Date, Calendar, or Long.
*
* @param str - the string to parse
* @param format - the format the date is in
* @param locale - the {@link Locale}
* @param timezone - the {@link TimeZone}
* @return the string as a {@link Date} or <code>null</code> if the
* parsing fails
* @see #getDateFormat
* @see SimpleDateFormat#parse
*/
public static Date toDate(String str, String format,
Locale locale, TimeZone timezone)
{
try
{
//try parsing w/a customized SimpleDateFormat
DateFormat parser = getDateFormat(format, locale, timezone);
return parser.parse(str);
}
catch (Exception e)
{
return null;
}
}
public static Calendar toCalendar(Date date, Locale locale)
{
if (date == null)
{
return null;
}
Calendar cal;
if (locale == null)
{
cal = Calendar.getInstance();
}
else
{
cal = Calendar.getInstance(locale);
}
cal.setTime(date);
// HACK: Force all fields to update. see link for explanation of this.
//http://java.sun.com/j2se/1.4/docs/api/java/util/Calendar.html
cal.getTime();
return cal;
}
// ----------------- misc conversion methods ---------------
/**
* Converts objects to String in a more Tools-ish way than
* String.valueOf(Object), especially with nulls, Arrays and Collections.
* Null returns null, Arrays and Collections return their first value,
* or null if they have no values.
*
* @param value the object to be turned into a String
* @return the string value of the object or null if the value is null
* or it is an array whose first value is null
*/
public static String toString(Object value)
{
if (value instanceof String)
{
return (String)value;
}
if (value == null)
{
return null;
}
if (value.getClass().isArray())
{
if (Array.getLength(value) > 0)
{
// recurse on the first value
return toString(Array.get(value, 0));
}
return null;
}
return String.valueOf(value);
}
/**
* Returns the first value as a String, if any; otherwise returns null.
*
* @param values the Collection to be turned into a string
* @return the string value of the first object in the collection
* or null if the collection is empty
*/
public static String toString(Collection values)
{
if (values != null && !values.isEmpty())
{
// recurse on the first value
return toString(values.iterator().next());
}
return null;
}
/**
* Converts any Object to a boolean using {@link #toString(Object)}
* and {@link Boolean#valueOf(String)}.
*
* @param value the object to be converted
* @return a {@link Boolean} object for the specified value or
* <code>null</code> if the value is null or the conversion failed
*/
public static Boolean toBoolean(Object value)
{
if (value instanceof Boolean)
{
return (Boolean)value;
}
String s = toString(value);
return (s != null) ? Boolean.valueOf(s) : null;
}
/**
* Converts a string to a {@link Locale}
*
* @param value - the string to parse
* @return the {@link Locale} or <code>null</code> if the
* parsing fails
*/
public static Locale toLocale(String value)
{
if (value.indexOf('_') < 0)
{
return new Locale(value);
}
String[] params = value.split("_");
if (params.length == 2)
{
return new Locale(params[0], params[1]);
}
else if (params.length == 3)
{
return new Locale(params[0], params[1], params[2]);
}
else
{
// there's only 3 possible params, so this must be invalid
return null;
}
}
/**
* Converts a string to a {@link URL}. It will first try to
* treat the string as a File name, then a classpath resource,
* then finally as a literal URL. If none of these work, then
* this will return {@code null}.
*
* @param value - the string to parse
* @return the {@link URL} form of the string or {@code null}
* @see File
* @see ClassUtils#getResource(String,Object)
* @see URL
*/
public static URL toURL(String value)
{
return toURL(value, ConversionUtils.class);
}
/**
* Converts a string to a {@link URL}. It will first try to
* treat the string as a File name, then a classpath resource,
* then finally as a literal URL. If none of these work, then
* this will return {@code null}.
*
* @param value - the string to parse
* @param caller - the object or Class seeking the url
* @return the {@link URL} form of the string or {@code null}
* @see File
* @see ClassUtils#getResource(String,Object)
* @see URL
*/
public static URL toURL(String value, Object caller)
{
try
{
File file = new File(value);
if (file.exists())
{
return file.toURI().toURL();
}
}
catch (Exception e) {}
try
{
URL url = ClassUtils.getResource(value, caller);
if (url != null)
{
return url;
}
}
catch (Exception e) {}
try
{
return new URL(value);
}
catch (Exception e) {}
return null;
}
}