001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.beanutils2.locale;
019
020import java.lang.reflect.Array;
021import java.math.BigDecimal;
022import java.math.BigInteger;
023import java.util.Locale;
024import java.util.Map;
025
026import org.apache.commons.beanutils2.BeanUtils;
027import org.apache.commons.beanutils2.ConversionException;
028import org.apache.commons.beanutils2.locale.converters.BigDecimalLocaleConverter;
029import org.apache.commons.beanutils2.locale.converters.BigIntegerLocaleConverter;
030import org.apache.commons.beanutils2.locale.converters.ByteLocaleConverter;
031import org.apache.commons.beanutils2.locale.converters.DoubleLocaleConverter;
032import org.apache.commons.beanutils2.locale.converters.FloatLocaleConverter;
033import org.apache.commons.beanutils2.locale.converters.IntegerLocaleConverter;
034import org.apache.commons.beanutils2.locale.converters.LongLocaleConverter;
035import org.apache.commons.beanutils2.locale.converters.ShortLocaleConverter;
036import org.apache.commons.beanutils2.locale.converters.StringLocaleConverter;
037import org.apache.commons.beanutils2.sql.converters.locale.SqlDateLocaleConverter;
038import org.apache.commons.beanutils2.sql.converters.locale.SqlTimeLocaleConverter;
039import org.apache.commons.beanutils2.sql.converters.locale.SqlTimestampLocaleConverter;
040import org.apache.commons.logging.Log;
041import org.apache.commons.logging.LogFactory;
042
043/**
044 * <p>
045 * Utility methods for converting locale-sensitive String scalar values to objects of the specified Class, String arrays to arrays of the specified Class and
046 * object to locale-sensitive String scalar value.
047 * </p>
048 *
049 * <p>
050 * This class provides the implementations used by the static utility methods in {@link LocaleConvertUtils}.
051 * </p>
052 *
053 * <p>
054 * The actual {@link LocaleConverter} instance to be used can be registered for each possible destination Class. Unless you override them, standard
055 * {@link LocaleConverter} instances are provided for all of the following destination Classes:
056 * </p>
057 * <ul>
058 * <li>java.lang.BigDecimal</li>
059 * <li>java.lang.BigInteger</li>
060 * <li>byte and java.lang.Byte</li>
061 * <li>double and java.lang.Double</li>
062 * <li>float and java.lang.Float</li>
063 * <li>int and java.lang.Integer</li>
064 * <li>long and java.lang.Long</li>
065 * <li>short and java.lang.Short</li>
066 * <li>java.lang.String</li>
067 * <li>java.sql.Date</li>
068 * <li>java.sql.Time</li>
069 * <li>java.sql.Timestamp</li>
070 * </ul>
071 *
072 * <p>
073 * For backwards compatibility, the standard locale converters for primitive types (and the corresponding wrapper classes).
074 *
075 * If you prefer to have another {@link LocaleConverter} thrown instead, replace the standard {@link LocaleConverter} instances with ones created with the one
076 * of the appropriate constructors.
077 *
078 * It's important that {@link LocaleConverter} should be registered for the specified locale and Class (or primitive type).
079 *
080 * @since 1.7
081 */
082public class LocaleConvertUtilsBean {
083
084    /** The {@code Log} instance for this class. */
085    private static final Log LOG = LogFactory.getLog(LocaleConvertUtilsBean.class);
086
087    /**
088     * Gets singleton instance. This is the same as the instance used by the default {@link LocaleBeanUtilsBean} singleton.
089     *
090     * @return the singleton instance
091     */
092    public static LocaleConvertUtilsBean getInstance() {
093        return LocaleBeanUtilsBean.getLocaleBeanUtilsInstance().getLocaleConvertUtils();
094    }
095
096    /** The locale - default for conversion. */
097    private Locale defaultLocale = Locale.getDefault();
098
099    /** Indicate whether the pattern is localized or not */
100    private boolean applyLocalized;
101
102    /**
103     * Every entry of the mapConverters is:
104     * <ul>
105     * <li>key = locale</li>
106     * <li>value = map of converters for the certain locale.</li>
107     * <ul>
108     */
109    private final Map<Locale, Map<Class<?>, LocaleConverter<?>>> mapConverters;
110
111    /**
112     * Makes the state by default (deregisters all converters for all locales) and then registers default locale converters.
113     */
114    public LocaleConvertUtilsBean() {
115        mapConverters = BeanUtils.createCache();
116        deregister();
117    }
118
119    /**
120     * Convert the specified locale-sensitive value into a String.
121     *
122     * @param value The Value to be converted
123     * @return the converted value
124     * @throws ConversionException if thrown by an underlying Converter
125     */
126    public String convert(final Object value) {
127        return convert(value, defaultLocale, null);
128    }
129
130    /**
131     * Convert the specified locale-sensitive value into a String using the particular conversion pattern.
132     *
133     * @param value   The Value to be converted
134     * @param locale  The locale
135     * @param pattern The conversion pattern
136     * @return the converted value
137     * @throws ConversionException if thrown by an underlying Converter
138     */
139    public String convert(final Object value, final Locale locale, final String pattern) {
140        final LocaleConverter<String> converter = lookup(String.class, locale);
141        return converter.convert(String.class, value, pattern);
142    }
143
144    /**
145     * Convert the specified locale-sensitive value into a String using the conversion pattern.
146     *
147     * @param value   The Value to be converted
148     * @param pattern The conversion pattern
149     * @return the converted value
150     * @throws ConversionException if thrown by an underlying Converter
151     */
152    public String convert(final Object value, final String pattern) {
153        return convert(value, defaultLocale, pattern);
154    }
155
156    /**
157     * Convert the specified value to an object of the specified class (if possible). Otherwise, return a String representation of the value.
158     *
159     * @param value The String scalar value to be converted
160     * @param clazz The Data type to which this value should be converted.
161     * @return the converted value
162     * @throws ConversionException if thrown by an underlying Converter
163     */
164    public Object convert(final String value, final Class<?> clazz) {
165        return convert(value, clazz, defaultLocale, null);
166    }
167
168    /**
169     * Convert the specified value to an object of the specified class (if possible) using the conversion pattern. Otherwise, return a String representation of
170     * the value.
171     *
172     * @param value   The String scalar value to be converted
173     * @param clazz   The Data type to which this value should be converted.
174     * @param locale  The locale
175     * @param pattern The conversion pattern
176     * @return the converted value
177     * @throws ConversionException if thrown by an underlying Converter
178     */
179    public Object convert(final String value, final Class<?> clazz, final Locale locale, final String pattern) {
180        if (LOG.isDebugEnabled()) {
181            LOG.debug("Convert string " + value + " to class " + clazz.getName() + " using " + locale + " locale and " + pattern + " pattern");
182        }
183
184        Class<?> targetClass = clazz;
185        LocaleConverter converter = lookup(clazz, locale);
186
187        if (converter == null) {
188            converter = lookup(String.class, locale);
189            targetClass = String.class;
190        }
191        if (LOG.isTraceEnabled()) {
192            LOG.trace("  Using converter " + converter);
193        }
194
195        return converter.convert(targetClass, value, pattern);
196    }
197
198    /**
199     * Convert the specified value to an object of the specified class (if possible) using the conversion pattern. Otherwise, return a String representation of
200     * the value.
201     *
202     * @param value   The String scalar value to be converted
203     * @param clazz   The Data type to which this value should be converted.
204     * @param pattern The conversion pattern
205     * @return the converted value
206     * @throws ConversionException if thrown by an underlying Converter
207     */
208    public Object convert(final String value, final Class<?> clazz, final String pattern) {
209        return convert(value, clazz, defaultLocale, pattern);
210    }
211
212    /**
213     * Convert an array of specified values to an array of objects of the specified class (if possible) .
214     *
215     * @param values Value to be converted (may be null)
216     * @param clazz  Java array or element class to be converted to
217     * @return the converted value
218     * @throws ConversionException if thrown by an underlying Converter
219     */
220    public Object convert(final String[] values, final Class<?> clazz) {
221        return convert(values, clazz, getDefaultLocale(), null);
222    }
223
224    /**
225     * Convert an array of specified values to an array of objects of the specified class (if possible) using the conversion pattern.
226     *
227     * @param <T>     The result component type
228     * @param values  Value to be converted (may be null)
229     * @param clazz   Java array or element class to be converted to
230     * @param locale  The locale
231     * @param pattern The conversion pattern
232     * @return the converted value
233     * @throws ConversionException if thrown by an underlying Converter
234     */
235    public <T> T[] convert(final String[] values, final Class<T> clazz, final Locale locale, final String pattern) {
236        Class<?> type = clazz;
237        if (clazz.isArray()) {
238            type = clazz.getComponentType();
239        }
240        if (LOG.isDebugEnabled()) {
241            LOG.debug("Convert String[" + values.length + "] to class " + type.getName() + "[] using " + locale + " locale and " + pattern + " pattern");
242        }
243
244        final T[] array = (T[]) Array.newInstance(type, values.length);
245        for (int i = 0; i < values.length; i++) {
246            Array.set(array, i, convert(values[i], type, locale, pattern));
247        }
248        return array;
249    }
250
251    /**
252     * Convert an array of specified values to an array of objects of the specified class (if possible) using the conversion pattern.
253     *
254     * @param <T>     The array component type
255     * @param values  Value to be converted (may be null)
256     * @param clazz   Java array or element class to be converted to
257     * @param pattern The conversion pattern
258     * @return the converted value
259     * @throws ConversionException if thrown by an underlying Converter
260     */
261    public <T> T[] convert(final String[] values, final Class<T> clazz, final String pattern) {
262        return convert(values, clazz, getDefaultLocale(), pattern);
263    }
264
265    /**
266     * Create all {@link LocaleConverter} types for specified locale.
267     *
268     * @param locale The Locale
269     * @return The map instance contains the all {@link LocaleConverter} types for the specified locale.
270     */
271    protected Map<Class<?>, LocaleConverter<?>> create(final Locale locale) {
272        final Map<Class<?>, LocaleConverter<?>> converter = BeanUtils.createCache();
273
274        converter.put(BigDecimal.class, BigDecimalLocaleConverter.builder().setLocale(locale).setLocalizedPattern(applyLocalized).get());
275        converter.put(BigInteger.class, BigIntegerLocaleConverter.builder().setLocale(locale).setLocalizedPattern(applyLocalized).get());
276
277        converter.put(Byte.class, ByteLocaleConverter.builder().setLocale(locale).setLocalizedPattern(applyLocalized).get());
278        converter.put(Byte.TYPE, ByteLocaleConverter.builder().setLocale(locale).setLocalizedPattern(applyLocalized).get());
279
280        converter.put(Double.class, DoubleLocaleConverter.builder().setLocale(locale).setLocalizedPattern(applyLocalized).get());
281        converter.put(Double.TYPE, DoubleLocaleConverter.builder().setLocale(locale).setLocalizedPattern(applyLocalized).get());
282
283        converter.put(Float.class, FloatLocaleConverter.builder().setLocale(locale).setLocalizedPattern(applyLocalized).get());
284        converter.put(Float.TYPE, FloatLocaleConverter.builder().setLocale(locale).setLocalizedPattern(applyLocalized).get());
285
286        converter.put(Integer.class, IntegerLocaleConverter.builder().setLocale(locale).setLocalizedPattern(applyLocalized).get());
287        converter.put(Integer.TYPE, IntegerLocaleConverter.builder().setLocale(locale).setLocalizedPattern(applyLocalized).get());
288
289        converter.put(Long.class, LongLocaleConverter.builder().setLocale(locale).setLocalizedPattern(applyLocalized).get());
290        converter.put(Long.TYPE, LongLocaleConverter.builder().setLocale(locale).setLocalizedPattern(applyLocalized).get());
291
292        converter.put(Short.class, ShortLocaleConverter.builder().setLocale(locale).setLocalizedPattern(applyLocalized).get());
293        converter.put(Short.TYPE, ShortLocaleConverter.builder().setLocale(locale).setLocalizedPattern(applyLocalized).get());
294
295        converter.put(String.class, StringLocaleConverter.builder().setLocale(locale).setLocalizedPattern(applyLocalized).get());
296
297        // conversion format patterns of java.sql.* types should correspond to default
298        // behavior of toString and valueOf methods of these classes
299        converter.put(java.sql.Date.class, SqlDateLocaleConverter.builder().setLocale(locale).setPattern("yyyy-MM-dd").get());
300        converter.put(java.sql.Time.class, SqlTimeLocaleConverter.builder().setLocale(locale).setPattern("HH:mm:ss").get());
301        converter.put(java.sql.Timestamp.class, SqlTimestampLocaleConverter.builder().setLocale(locale).setPattern("yyyy-MM-dd HH:mm:ss.S").get());
302
303        return converter;
304    }
305
306    /**
307     * Remove any registered {@link LocaleConverter}.
308     */
309    public void deregister() {
310        final Map<Class<?>, LocaleConverter<?>> defaultConverter = lookup(defaultLocale);
311        mapConverters.clear();
312        mapConverters.put(defaultLocale, defaultConverter);
313    }
314
315    /**
316     * Remove any registered {@link LocaleConverter} for the specified locale and Class.
317     *
318     * @param clazz  Class for which to remove a registered Converter
319     * @param locale The locale
320     */
321    public void deregister(final Class<?> clazz, final Locale locale) {
322        lookup(locale).remove(clazz);
323    }
324
325    /**
326     * Remove any registered {@link LocaleConverter} for the specified locale
327     *
328     * @param locale The locale
329     */
330    public void deregister(final Locale locale) {
331        mapConverters.remove(locale);
332    }
333
334    /**
335     * getter for applyLocalized
336     *
337     * @return {@code true} if pattern is localized, otherwise {@code false}
338     */
339    public boolean getApplyLocalized() {
340        return applyLocalized;
341    }
342
343    /**
344     * getter for defaultLocale.
345     *
346     * @return the default locale
347     */
348    public Locale getDefaultLocale() {
349        return defaultLocale;
350    }
351
352    /**
353     * Look up and return any registered {@link LocaleConverter} for the specified destination class and locale; if there is no registered Converter, return
354     * {@code null}.
355     *
356     * @param <T>    The converter type.
357     * @param clazz  Class for which to return a registered Converter
358     * @param locale The Locale
359     * @return The registered locale Converter, if any
360     */
361    public <T> LocaleConverter<T> lookup(final Class<T> clazz, final Locale locale) {
362        final LocaleConverter<T> converter = (LocaleConverter<T>) lookup(locale).get(clazz);
363
364        if (LOG.isTraceEnabled()) {
365            LOG.trace("LocaleConverter:" + converter);
366        }
367
368        return converter;
369    }
370
371    /**
372     * Look up and return any registered map instance for the specified locale; if there is no registered one, return {@code null}.
373     *
374     * @param locale The Locale
375     * @return The map instance contains the all {@link LocaleConverter} types for the specified locale.
376     */
377    protected Map<Class<?>, LocaleConverter<?>> lookup(final Locale locale) {
378        return mapConverters.computeIfAbsent(locale == null ? defaultLocale : locale, this::create);
379    }
380
381    /**
382     * Register a custom {@link LocaleConverter} for the specified destination {@code Class}, replacing any previously registered converter.
383     *
384     * @param <T>       The converter type.
385     * @param converter The LocaleConverter to be registered
386     * @param clazz     The Destination class for conversions performed by this Converter
387     * @param locale    The locale
388     */
389    public <T> void register(final LocaleConverter<T> converter, final Class<T> clazz, final Locale locale) {
390        lookup(locale).put(clazz, converter);
391    }
392
393    /**
394     * setter for applyLocalized
395     *
396     * @param newApplyLocalized {@code true} if pattern is localized, otherwise {@code false}
397     */
398    public void setApplyLocalized(final boolean newApplyLocalized) {
399        applyLocalized = newApplyLocalized;
400    }
401
402    /**
403     * setter for defaultLocale.
404     *
405     * @param locale the default locale
406     */
407    public void setDefaultLocale(final Locale locale) {
408        if (locale == null) {
409            defaultLocale = Locale.getDefault();
410        } else {
411            defaultLocale = locale;
412        }
413    }
414}