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.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
318        * specified class (if possible) .
319        *
320        * @param values Value to be converted (may be null)
321        * @param clazz Java array or element class to be converted to
322        * @return the converted value
323         * @throws org.apache.commons.beanutils.ConversionException if thrown by an
324         * underlying Converter
325        */
326       public Object convert(final String[] values, final Class<?> clazz) {
327
328           return convert(values, clazz, getDefaultLocale(), null);
329       }
330
331    /**
332     * Convert an array of specified values to an array of objects of the
333     * specified class (if possible) using the convertion pattern.
334     *
335     * @param values Value to be converted (may be null)
336     * @param clazz Java array or element class to be converted to
337     * @param locale The locale
338     * @param pattern The convertion pattern
339     * @return the converted value
340     * @throws org.apache.commons.beanutils.ConversionException if thrown by an
341     * underlying Converter
342     */
343    public Object convert(final String[] values, final Class<?> clazz, final Locale locale, final String pattern) {
344
345        Class<?> type = clazz;
346        if (clazz.isArray()) {
347            type = clazz.getComponentType();
348        }
349        if (log.isDebugEnabled()) {
350            log.debug("Convert String[" + values.length + "] to class " +
351                    type.getName() + "[] using " + locale +
352                    " locale and " + pattern + " pattern");
353        }
354
355        final Object array = Array.newInstance(type, values.length);
356        for (int i = 0; i < values.length; i++) {
357            Array.set(array, i, convert(values[i], type, locale, pattern));
358        }
359
360        return array;
361    }
362
363    /**
364     * Convert an array of specified values to an array of objects of the
365     * specified class (if possible) using the convertion pattern.
366     *
367     * @param values Value to be converted (may be null)
368     * @param clazz Java array or element class to be converted to
369     * @param pattern The convertion pattern
370     * @return the converted value
371     * @throws org.apache.commons.beanutils.ConversionException if thrown by an
372     * underlying Converter
373     */
374    public Object convert(final String[] values, final Class<?> clazz, final String pattern) {
375
376        return convert(values, clazz, getDefaultLocale(), pattern);
377    }
378
379    /**
380     *  Create all {@link LocaleConverter} types for specified locale.
381     *
382     * @param locale The Locale
383     * @return The FastHashMap instance contains the all {@link LocaleConverter} types
384     *  for the specified locale.
385     * @deprecated This method will be modified to return a Map in the next release.
386     */
387    @Deprecated
388    protected FastHashMap create(final Locale locale) {
389
390        final FastHashMap converter = new DelegateFastHashMap(BeanUtils.createCache());
391        converter.setFast(false);
392
393        converter.put(BigDecimal.class, new BigDecimalLocaleConverter(locale, applyLocalized));
394        converter.put(BigInteger.class, new BigIntegerLocaleConverter(locale, applyLocalized));
395
396        converter.put(Byte.class, new ByteLocaleConverter(locale, applyLocalized));
397        converter.put(Byte.TYPE, new ByteLocaleConverter(locale, applyLocalized));
398
399        converter.put(Double.class, new DoubleLocaleConverter(locale, applyLocalized));
400        converter.put(Double.TYPE, new DoubleLocaleConverter(locale, applyLocalized));
401
402        converter.put(Float.class, new FloatLocaleConverter(locale, applyLocalized));
403        converter.put(Float.TYPE, new FloatLocaleConverter(locale, applyLocalized));
404
405        converter.put(Integer.class, new IntegerLocaleConverter(locale, applyLocalized));
406        converter.put(Integer.TYPE, new IntegerLocaleConverter(locale, applyLocalized));
407
408        converter.put(Long.class, new LongLocaleConverter(locale, applyLocalized));
409        converter.put(Long.TYPE, new LongLocaleConverter(locale, applyLocalized));
410
411        converter.put(Short.class, new ShortLocaleConverter(locale, applyLocalized));
412        converter.put(Short.TYPE, new ShortLocaleConverter(locale, applyLocalized));
413
414        converter.put(String.class, new StringLocaleConverter(locale, applyLocalized));
415
416        // conversion format patterns of java.sql.* types should correspond to default
417        // behavior of toString and valueOf methods of these classes
418        converter.put(java.sql.Date.class, new SqlDateLocaleConverter(locale, "yyyy-MM-dd"));
419        converter.put(java.sql.Time.class, new SqlTimeLocaleConverter(locale, "HH:mm:ss"));
420        converter.put(java.sql.Timestamp.class, new SqlTimestampLocaleConverter(locale, "yyyy-MM-dd HH:mm:ss.S"));
421
422        converter.setFast(true);
423
424        return converter;
425    }
426
427   /**
428 * Remove any registered {@link LocaleConverter}.
429 */
430public void deregister() {
431
432    final FastHashMap defaultConverter = lookup(defaultLocale);
433
434    mapConverters.setFast(false);
435
436    mapConverters.clear();
437    mapConverters.put(defaultLocale, defaultConverter);
438
439    mapConverters.setFast(true);
440}
441
442    /**
443     * Remove any registered {@link LocaleConverter} for the specified locale and Class.
444     *
445     * @param clazz Class for which to remove a registered Converter
446     * @param locale The locale
447     */
448    public void deregister(final Class<?> clazz, final Locale locale) {
449
450        lookup(locale).remove(clazz);
451    }
452
453    /**
454     * Remove any registered {@link LocaleConverter} for the specified locale
455     *
456     * @param locale The locale
457     */
458    public void deregister(final Locale locale) {
459
460        mapConverters.remove(locale);
461    }
462
463    /**
464     * getter for applyLocalized
465     *
466     * @return <code>true</code> if pattern is localized,
467     * otherwise <code>false</code>
468     */
469    public boolean getApplyLocalized() {
470        return applyLocalized;
471    }
472
473    /**
474     * getter for defaultLocale.
475     * @return the default locale
476     */
477    public Locale getDefaultLocale() {
478
479        return defaultLocale;
480    }
481
482    /**
483     * Look up and return any registered {@link LocaleConverter} for the specified
484     * destination class and locale; if there is no registered Converter, return
485     * <code>null</code>.
486     *
487     * @param clazz Class for which to return a registered Converter
488     * @param locale The Locale
489     * @return The registered locale Converter, if any
490     */
491    public LocaleConverter lookup(final Class<?> clazz, final Locale locale) {
492
493        final LocaleConverter converter = (LocaleConverter) lookup(locale).get(clazz);
494
495        if (log.isTraceEnabled()) {
496            log.trace("LocaleConverter:" + converter);
497        }
498
499        return converter;
500    }
501
502    /**
503     * Look up and return any registered FastHashMap instance for the specified locale;
504     * if there is no registered one, return <code>null</code>.
505     *
506     * @param locale The Locale
507     * @return The FastHashMap instance contains the all {@link LocaleConverter} types for
508     *  the specified locale.
509     * @deprecated This method will be modified to return a Map in the next release.
510     */
511    @Deprecated
512    protected FastHashMap lookup(final Locale locale) {
513        FastHashMap localeConverters;
514
515        if (locale == null) {
516            localeConverters = (FastHashMap) mapConverters.get(defaultLocale);
517        }
518        else {
519            localeConverters = (FastHashMap) mapConverters.get(locale);
520
521            if (localeConverters == null) {
522                localeConverters = create(locale);
523                mapConverters.put(locale, localeConverters);
524            }
525        }
526
527        return localeConverters;
528    }
529
530    /**
531     * Register a custom {@link LocaleConverter} for the specified destination
532     * <code>Class</code>, replacing any previously registered converter.
533     *
534     * @param converter The LocaleConverter to be registered
535     * @param clazz The Destination class for conversions performed by this
536     *  Converter
537     * @param locale The locale
538     */
539    public void register(final LocaleConverter converter, final Class<?> clazz, final Locale locale) {
540
541        lookup(locale).put(clazz, converter);
542    }
543
544    /**
545     * setter for applyLocalized
546     *
547     * @param newApplyLocalized <code>true</code> if pattern is localized,
548     * otherwise <code>false</code>
549     */
550    public void setApplyLocalized(final boolean newApplyLocalized) {
551        applyLocalized = newApplyLocalized;
552    }
553
554    /**
555     * setter for defaultLocale.
556     * @param locale the default locale
557     */
558    public void setDefaultLocale(final Locale locale) {
559
560        if (locale == null) {
561            defaultLocale = Locale.getDefault();
562        }
563        else {
564            defaultLocale = locale;
565        }
566    }
567}