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 *      https://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.beanutils.locale;
019
020import java.lang.reflect.Array;
021import java.math.BigDecimal;
022import java.math.BigInteger;
023import java.util.Collection;
024import java.util.Locale;
025import java.util.Map;
026import java.util.Set;
027
028import org.apache.commons.beanutils.BeanUtils;
029import org.apache.commons.beanutils.locale.converters.BigDecimalLocaleConverter;
030import org.apache.commons.beanutils.locale.converters.BigIntegerLocaleConverter;
031import org.apache.commons.beanutils.locale.converters.ByteLocaleConverter;
032import org.apache.commons.beanutils.locale.converters.DoubleLocaleConverter;
033import org.apache.commons.beanutils.locale.converters.FloatLocaleConverter;
034import org.apache.commons.beanutils.locale.converters.IntegerLocaleConverter;
035import org.apache.commons.beanutils.locale.converters.LongLocaleConverter;
036import org.apache.commons.beanutils.locale.converters.ShortLocaleConverter;
037import org.apache.commons.beanutils.locale.converters.SqlDateLocaleConverter;
038import org.apache.commons.beanutils.locale.converters.SqlTimeLocaleConverter;
039import org.apache.commons.beanutils.locale.converters.SqlTimestampLocaleConverter;
040import org.apache.commons.beanutils.locale.converters.StringLocaleConverter;
041import org.apache.commons.collections.FastHashMap;
042import org.apache.commons.logging.Log;
043import org.apache.commons.logging.LogFactory;
044
045/**
046 * <p>Utility methods for converting locale-sensitive String scalar values to objects of the
047 * specified Class, String arrays to arrays of the specified Class and
048 * object to locale-sensitive String scalar value.</p>
049 *
050 * <p>This class provides the implementations used by the static utility methods in
051 * {@link LocaleConvertUtils}.</p>
052 *
053 * <p>The actual {@link LocaleConverter} instance to be used
054 * can be registered for each possible destination Class. Unless you override them, standard
055 * {@link LocaleConverter} instances are provided for all of the following
056 * destination Classes:</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>For backwards compatibility, the standard locale converters
073 * for primitive types (and the corresponding wrapper classes).
074 *
075 * If you prefer to have another {@link LocaleConverter}
076 * thrown instead, replace the standard {@link LocaleConverter} instances
077 * with ones created with the one of the appropriate constructors.
078 *
079 * It's important that {@link LocaleConverter} should be registered for
080 * the specified locale and Class (or primitive type).
081 *
082 * @since 1.7
083 */
084public class LocaleConvertUtilsBean {
085
086    /**
087     * FastHashMap implementation that uses WeakReferences to overcome
088     * memory leak problems.
089     *
090     * This is a hack to retain binary compatibility with previous
091     * releases (where FastHashMap is exposed in the API), but
092     * use WeakHashMap to resolve memory leaks.
093     */
094    private static class DelegateFastHashMap extends FastHashMap {
095
096        private static final long serialVersionUID = 1L;
097        private final Map<Object, Object> map;
098
099        private DelegateFastHashMap(final Map<Object, Object> map) {
100            this.map = map;
101        }
102        @Override
103        public void clear() {
104            map.clear();
105        }
106        @Override
107        public boolean containsKey(final Object key) {
108            return map.containsKey(key);
109        }
110        @Override
111        public boolean containsValue(final Object value) {
112            return map.containsValue(value);
113        }
114        @Override
115        public Set<Map.Entry<Object, Object>> entrySet() {
116            return map.entrySet();
117        }
118        @Override
119        public boolean equals(final Object o) {
120            return map.equals(o);
121        }
122        @Override
123        public Object get(final Object key) {
124            return map.get(key);
125        }
126        @Override
127        public boolean getFast() {
128            return BeanUtils.getCacheFast(map);
129        }
130        @Override
131        public int hashCode() {
132            return map.hashCode();
133        }
134        @Override
135        public boolean isEmpty() {
136            return map.isEmpty();
137        }
138        @Override
139        public Set<Object> keySet() {
140            return map.keySet();
141        }
142        @Override
143        public Object put(final Object key, final Object value) {
144            return map.put(key, value);
145        }
146        @SuppressWarnings({ "rawtypes", "unchecked" })
147        // we operate on very generic types (<Object, Object>), so there is
148        // no need for doing type checks
149        @Override
150        public void putAll(final Map m) {
151            map.putAll(m);
152        }
153        @Override
154        public Object remove(final Object key) {
155            return map.remove(key);
156        }
157        @Override
158        public void setFast(final boolean fast) {
159            BeanUtils.setCacheFast(map, fast);
160        }
161        @Override
162        public int size() {
163            return map.size();
164        }
165        @Override
166        public Collection<Object> values() {
167            return map.values();
168        }
169    }
170
171    /**
172     * Gets singleton instance.
173     * This is the same as the instance used by the default {@link LocaleBeanUtilsBean} singleton.
174     * @return the singleton instance
175     */
176    public static LocaleConvertUtilsBean getInstance() {
177        return LocaleBeanUtilsBean.getLocaleBeanUtilsInstance().getLocaleConvertUtils();
178    }
179
180    /** The locale - default for convertion. */
181    private Locale defaultLocale = Locale.getDefault();
182
183    /** Indicate whether the pattern is localized or not */
184    private boolean applyLocalized;
185
186    /** The <code>Log</code> instance for this class. */
187    private final Log log = LogFactory.getLog(LocaleConvertUtils.class);
188
189    /** Every entry of the mapConverters is:
190     *  key = locale
191     *  value = FastHashMap of converters for the certain locale.
192     */
193    private final FastHashMap mapConverters = new DelegateFastHashMap(BeanUtils.createCache());
194
195    /**
196     *  Makes the state by default (deregisters all converters for all locales)
197     *  and then registers default locale converters.
198     */
199    public LocaleConvertUtilsBean() {
200        mapConverters.setFast(false);
201        deregister();
202        mapConverters.setFast(true);
203    }
204
205    /**
206     * Convert the specified locale-sensitive value into a String.
207     *
208     * @param value The Value to be converted
209     * @return the converted value
210     * @throws org.apache.commons.beanutils.ConversionException if thrown by an
211     * underlying Converter
212     */
213    public String convert(final Object value) {
214        return convert(value, defaultLocale, null);
215    }
216
217    /**
218     * Convert the specified locale-sensitive value into a String
219     * using the paticular convertion pattern.
220     *
221     * @param value The Value to be converted
222     * @param locale The locale
223     * @param pattern The convertion pattern
224     * @return the converted value
225     * @throws org.apache.commons.beanutils.ConversionException if thrown by an
226     * underlying Converter
227     */
228    public String convert(final Object value, final Locale locale, final String pattern) {
229
230        final LocaleConverter converter = lookup(String.class, locale);
231
232        return converter.convert(String.class, value, pattern);
233    }
234
235    /**
236     * Convert the specified locale-sensitive value into a String
237     * using the conversion pattern.
238     *
239     * @param value The Value to be converted
240     * @param pattern       The convertion pattern
241     * @return the converted value
242     * @throws org.apache.commons.beanutils.ConversionException if thrown by an
243     * underlying Converter
244     */
245    public String convert(final Object value, final String pattern) {
246        return convert(value, defaultLocale, pattern);
247    }
248
249    /**
250     * Convert the specified value to an object of the specified class (if
251     * possible).  Otherwise, return a String representation of the value.
252     *
253     * @param value The String scalar value to be converted
254     * @param clazz The Data type to which this value should be converted.
255     * @return the converted value
256     * @throws org.apache.commons.beanutils.ConversionException if thrown by an
257     * underlying Converter
258     */
259    public Object convert(final String value, final Class<?> clazz) {
260
261        return convert(value, clazz, defaultLocale, null);
262    }
263
264    /**
265     * Convert the specified value to an object of the specified class (if
266     * possible) using the convertion pattern. Otherwise, return a String
267     * representation of the value.
268     *
269     * @param value The String scalar value to be converted
270     * @param clazz The Data type to which this value should be converted.
271     * @param locale The locale
272     * @param pattern The convertion pattern
273     * @return the converted value
274     * @throws org.apache.commons.beanutils.ConversionException if thrown by an
275     * underlying Converter
276     */
277    public Object convert(final String value, final Class<?> clazz, final Locale locale, final String pattern) {
278
279        if (log.isDebugEnabled()) {
280            log.debug("Convert string " + value + " to class " +
281                    clazz.getName() + " using " + locale +
282                    " locale and " + pattern + " pattern");
283        }
284
285        Class<?> targetClass = clazz;
286        LocaleConverter converter = lookup(clazz, locale);
287
288        if (converter == null) {
289            converter = lookup(String.class, locale);
290            targetClass = String.class;
291        }
292        if (log.isTraceEnabled()) {
293            log.trace("  Using converter " + converter);
294        }
295
296        return converter.convert(targetClass, value, pattern);
297    }
298
299    /**
300     * Convert the specified value to an object of the specified class (if
301     * possible) using the convertion pattern. Otherwise, return a String
302     * representation of the value.
303     *
304     * @param value The String scalar value to be converted
305     * @param clazz The Data type to which this value should be converted.
306     * @param pattern The convertion pattern
307     * @return the converted value
308     * @throws org.apache.commons.beanutils.ConversionException if thrown by an
309     * underlying Converter
310     */
311    public Object convert(final String value, final Class<?> clazz, final String pattern) {
312
313        return convert(value, clazz, defaultLocale, pattern);
314    }
315
316    /**
317     * Convert an array of specified values to an array of objects of the specified class (if possible).
318     *
319     * @param values Value to be converted (may be null)
320     * @param clazz  Java array or element class to be converted to
321     * @return the converted value
322     * @throws org.apache.commons.beanutils.ConversionException if thrown by an underlying Converter
323     */
324    public Object convert(final String[] values, final Class<?> clazz) {
325        return convert(values, clazz, getDefaultLocale(), null);
326    }
327
328    /**
329     * Convert an array of specified values to an array of objects of the
330     * specified class (if possible) using the convertion pattern.
331     *
332     * @param values Value to be converted (may be null)
333     * @param clazz Java array or element class to be converted to
334     * @param locale The locale
335     * @param pattern The convertion pattern
336     * @return the converted value
337     * @throws org.apache.commons.beanutils.ConversionException if thrown by an
338     * underlying Converter
339     */
340    public Object convert(final String[] values, final Class<?> clazz, final Locale locale, final String pattern) {
341
342        Class<?> type = clazz;
343        if (clazz.isArray()) {
344            type = clazz.getComponentType();
345        }
346        if (log.isDebugEnabled()) {
347            log.debug("Convert String[" + values.length + "] to class " +
348                    type.getName() + "[] using " + locale +
349                    " locale and " + pattern + " pattern");
350        }
351
352        final Object array = Array.newInstance(type, values.length);
353        for (int i = 0; i < values.length; i++) {
354            Array.set(array, i, convert(values[i], type, locale, pattern));
355        }
356
357        return array;
358    }
359
360    /**
361     * Convert an array of specified values to an array of objects of the
362     * specified class (if possible) using the convertion pattern.
363     *
364     * @param values Value to be converted (may be null)
365     * @param clazz Java array or element class to be converted to
366     * @param pattern The convertion pattern
367     * @return the converted value
368     * @throws org.apache.commons.beanutils.ConversionException if thrown by an
369     * underlying Converter
370     */
371    public Object convert(final String[] values, final Class<?> clazz, final String pattern) {
372
373        return convert(values, clazz, getDefaultLocale(), pattern);
374    }
375
376    /**
377     *  Create all {@link LocaleConverter} types for specified locale.
378     *
379     * @param locale The Locale
380     * @return The FastHashMap instance contains the all {@link LocaleConverter} types
381     *  for the specified locale.
382     * @deprecated This method will be modified to return a Map in the next release.
383     */
384    @Deprecated
385    protected FastHashMap create(final Locale locale) {
386
387        final FastHashMap converter = new DelegateFastHashMap(BeanUtils.createCache());
388        converter.setFast(false);
389
390        converter.put(BigDecimal.class, new BigDecimalLocaleConverter(locale, applyLocalized));
391        converter.put(BigInteger.class, new BigIntegerLocaleConverter(locale, applyLocalized));
392
393        converter.put(Byte.class, new ByteLocaleConverter(locale, applyLocalized));
394        converter.put(Byte.TYPE, new ByteLocaleConverter(locale, applyLocalized));
395
396        converter.put(Double.class, new DoubleLocaleConverter(locale, applyLocalized));
397        converter.put(Double.TYPE, new DoubleLocaleConverter(locale, applyLocalized));
398
399        converter.put(Float.class, new FloatLocaleConverter(locale, applyLocalized));
400        converter.put(Float.TYPE, new FloatLocaleConverter(locale, applyLocalized));
401
402        converter.put(Integer.class, new IntegerLocaleConverter(locale, applyLocalized));
403        converter.put(Integer.TYPE, new IntegerLocaleConverter(locale, applyLocalized));
404
405        converter.put(Long.class, new LongLocaleConverter(locale, applyLocalized));
406        converter.put(Long.TYPE, new LongLocaleConverter(locale, applyLocalized));
407
408        converter.put(Short.class, new ShortLocaleConverter(locale, applyLocalized));
409        converter.put(Short.TYPE, new ShortLocaleConverter(locale, applyLocalized));
410
411        converter.put(String.class, new StringLocaleConverter(locale, applyLocalized));
412
413        // conversion format patterns of java.sql.* types should correspond to default
414        // behavior of toString and valueOf methods of these classes
415        converter.put(java.sql.Date.class, new SqlDateLocaleConverter(locale, "yyyy-MM-dd"));
416        converter.put(java.sql.Time.class, new SqlTimeLocaleConverter(locale, "HH:mm:ss"));
417        converter.put(java.sql.Timestamp.class, new SqlTimestampLocaleConverter(locale, "yyyy-MM-dd HH:mm:ss.S"));
418
419        converter.setFast(true);
420
421        return converter;
422    }
423
424   /**
425 * Remove any registered {@link LocaleConverter}.
426 */
427public void deregister() {
428
429    final FastHashMap defaultConverter = lookup(defaultLocale);
430
431    mapConverters.setFast(false);
432
433    mapConverters.clear();
434    mapConverters.put(defaultLocale, defaultConverter);
435
436    mapConverters.setFast(true);
437}
438
439    /**
440     * Remove any registered {@link LocaleConverter} for the specified locale and Class.
441     *
442     * @param clazz Class for which to remove a registered Converter
443     * @param locale The locale
444     */
445    public void deregister(final Class<?> clazz, final Locale locale) {
446
447        lookup(locale).remove(clazz);
448    }
449
450    /**
451     * Remove any registered {@link LocaleConverter} for the specified locale
452     *
453     * @param locale The locale
454     */
455    public void deregister(final Locale locale) {
456
457        mapConverters.remove(locale);
458    }
459
460    /**
461     * getter for applyLocalized
462     *
463     * @return <code>true</code> if pattern is localized,
464     * otherwise <code>false</code>
465     */
466    public boolean getApplyLocalized() {
467        return applyLocalized;
468    }
469
470    /**
471     * getter for defaultLocale.
472     * @return the default locale
473     */
474    public Locale getDefaultLocale() {
475
476        return defaultLocale;
477    }
478
479    /**
480     * Look up and return any registered {@link LocaleConverter} for the specified
481     * destination class and locale; if there is no registered Converter, return
482     * <code>null</code>.
483     *
484     * @param clazz Class for which to return a registered Converter
485     * @param locale The Locale
486     * @return The registered locale Converter, if any
487     */
488    public LocaleConverter lookup(final Class<?> clazz, final Locale locale) {
489
490        final LocaleConverter converter = (LocaleConverter) lookup(locale).get(clazz);
491
492        if (log.isTraceEnabled()) {
493            log.trace("LocaleConverter:" + converter);
494        }
495
496        return converter;
497    }
498
499    /**
500     * Look up and return any registered FastHashMap instance for the specified locale;
501     * if there is no registered one, return <code>null</code>.
502     *
503     * @param locale The Locale
504     * @return The FastHashMap instance contains the all {@link LocaleConverter} types for
505     *  the specified locale.
506     * @deprecated This method will be modified to return a Map in the next release.
507     */
508    @Deprecated
509    protected FastHashMap lookup(final Locale locale) {
510        FastHashMap localeConverters;
511
512        if (locale == null) {
513            localeConverters = (FastHashMap) mapConverters.get(defaultLocale);
514        }
515        else {
516            localeConverters = (FastHashMap) mapConverters.get(locale);
517
518            if (localeConverters == null) {
519                localeConverters = create(locale);
520                mapConverters.put(locale, localeConverters);
521            }
522        }
523
524        return localeConverters;
525    }
526
527    /**
528     * Register a custom {@link LocaleConverter} for the specified destination
529     * <code>Class</code>, replacing any previously registered converter.
530     *
531     * @param converter The LocaleConverter to be registered
532     * @param clazz The Destination class for conversions performed by this
533     *  Converter
534     * @param locale The locale
535     */
536    public void register(final LocaleConverter converter, final Class<?> clazz, final Locale locale) {
537
538        lookup(locale).put(clazz, converter);
539    }
540
541    /**
542     * setter for applyLocalized
543     *
544     * @param newApplyLocalized <code>true</code> if pattern is localized,
545     * otherwise <code>false</code>
546     */
547    public void setApplyLocalized(final boolean newApplyLocalized) {
548        applyLocalized = newApplyLocalized;
549    }
550
551    /**
552     * setter for defaultLocale.
553     * @param locale the default locale
554     */
555    public void setDefaultLocale(final Locale locale) {
556
557        if (locale == null) {
558            defaultLocale = Locale.getDefault();
559        }
560        else {
561            defaultLocale = locale;
562        }
563    }
564}