DateLocaleConverter.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */

  17. package org.apache.commons.beanutils2.locale.converters;

  18. import java.text.DateFormat;
  19. import java.text.DateFormatSymbols;
  20. import java.text.ParseException;
  21. import java.text.ParsePosition;
  22. import java.text.SimpleDateFormat;
  23. import java.util.Calendar;
  24. import java.util.Date;
  25. import java.util.Locale;

  26. import org.apache.commons.beanutils2.ConversionException;
  27. import org.apache.commons.beanutils2.locale.BaseLocaleConverter;
  28. import org.apache.commons.beanutils2.locale.LocaleConverter;
  29. import org.apache.commons.logging.Log;
  30. import org.apache.commons.logging.LogFactory;

  31. /**
  32.  * Standard {@link org.apache.commons.beanutils2.locale.LocaleConverter} implementation that converts an incoming locale-sensitive String into a
  33.  * {@link java.util.Date} object, optionally using a default value or throwing a {@link org.apache.commons.beanutils2.ConversionException} if a conversion error
  34.  * occurs.
  35.  *
  36.  * @param <D> The Date type.
  37.  */
  38. public class DateLocaleConverter<D extends Date> extends BaseLocaleConverter<D> {

  39.     /**
  40.      * Builds instances of {@link DateLocaleConverter}.
  41.      *
  42.      * @param <B> The builder type.
  43.      * @param <D> The Date type.
  44.      */
  45.     public static class Builder<B extends Builder<B, D>, D extends Date> extends BaseLocaleConverter.Builder<B, D> {

  46.         /** Should the date conversion be lenient? */
  47.         private boolean lenient;

  48.         /**
  49.          * Constructs a new instance.
  50.          */
  51.         public Builder() {
  52.             // empty
  53.         }

  54.         /**
  55.          * Gets a new instance.
  56.          * <p>
  57.          * Defaults construct a {@link LocaleConverter} that will throw a {@link ConversionException} if a conversion error occurs. The locale is the default
  58.          * locale for this instance of the Java Virtual Machine and an unlocalized pattern is used for the conversion.
  59.          * </p>
  60.          *
  61.          * @return a new instance.
  62.          */
  63.         @Override
  64.         public DateLocaleConverter<D> get() {
  65.             return new DateLocaleConverter<>(defaultValue, locale, pattern, useDefault || defaultValue != null, localizedPattern, lenient);
  66.         }

  67.         /**
  68.          * Tests whether date formatting is lenient.
  69.          *
  70.          * @return true if the {@code DateFormat} used for formatting is lenient
  71.          * @see java.text.DateFormat#isLenient()
  72.          */
  73.         public boolean isLenient() {
  74.             return lenient;
  75.         }

  76.         /**
  77.          * Sets the leniency policy.
  78.          *
  79.          * @param lenient the leniency policy.
  80.          * @return {@code this} instance.
  81.          */
  82.         public B setLenient(final boolean lenient) {
  83.             this.lenient = lenient;
  84.             return asThis();
  85.         }

  86.     }

  87.     /**
  88.      * Default Pattern Characters
  89.      */
  90.     private static final String DEFAULT_PATTERN_CHARS = DateLocaleConverter.initDefaultChars();

  91.     /** All logging goes through this logger */
  92.     private static final Log LOG = LogFactory.getLog(DateLocaleConverter.class);

  93.     /**
  94.      * Constructs a new builder.
  95.      *
  96.      * @param <B> The builder type.
  97.      * @param <D> The Date type.
  98.      * @return a new builder.
  99.      */
  100.     @SuppressWarnings("unchecked")
  101.     public static <B extends Builder<B, D>, D extends Date> B builder() {
  102.         return (B) new Builder<>();
  103.     }

  104.     /**
  105.      * This method is called at class initialization time to define the value for constant member DEFAULT_PATTERN_CHARS. All other methods needing this data
  106.      * should just read that constant.
  107.      */
  108.     private static String initDefaultChars() {
  109.         return new DateFormatSymbols(Locale.US).getLocalPatternChars();
  110.     }

  111.     /** Should the date conversion be lenient? */
  112.     private final boolean isLenient;

  113.     /**
  114.      * Constructs a new instance.
  115.      *
  116.      * @param defaultValue default value.
  117.      * @param locale       locale.
  118.      * @param pattern      pattern.
  119.      * @param useDefault   use the default.
  120.      * @param locPattern   localized pattern.
  121.      * @param lenient      leniency policy.
  122.      */
  123.     protected DateLocaleConverter(final D defaultValue, final Locale locale, final String pattern, final boolean useDefault, final boolean locPattern,
  124.             final boolean lenient) {
  125.         super(defaultValue, locale, pattern, useDefault, locPattern);
  126.         this.isLenient = lenient;
  127.     }

  128.     /**
  129.      * Converts a pattern from a localized format to the default format.
  130.      *
  131.      * @param locale           The locale
  132.      * @param localizedPattern The pattern in 'local' symbol format
  133.      * @return pattern in 'default' symbol format
  134.      */
  135.     private String convertLocalizedPattern(final String localizedPattern, final Locale locale) {
  136.         if (localizedPattern == null) {
  137.             return null;
  138.         }

  139.         // Note that this is a little obtuse.
  140.         // However, it is the best way that anyone can come up with
  141.         // that works with some 1.4 series JVM.

  142.         // Get the symbols for the localized pattern
  143.         final DateFormatSymbols localizedSymbols = new DateFormatSymbols(locale);
  144.         final String localChars = localizedSymbols.getLocalPatternChars();

  145.         if (DEFAULT_PATTERN_CHARS.equals(localChars)) {
  146.             return localizedPattern;
  147.         }

  148.         // Convert the localized pattern to default
  149.         String convertedPattern = null;
  150.         try {
  151.             convertedPattern = convertPattern(localizedPattern, localChars, DEFAULT_PATTERN_CHARS);
  152.         } catch (final Exception ex) {
  153.             if (LOG.isDebugEnabled()) {
  154.                 LOG.debug("Converting pattern '" + localizedPattern + "' for " + locale, ex);
  155.             }
  156.         }
  157.         return convertedPattern;
  158.     }

  159.     /**
  160.      * Converts a Pattern from one character set to another.
  161.      */
  162.     private String convertPattern(final String pattern, final String fromChars, final String toChars) {
  163.         final StringBuilder converted = new StringBuilder();
  164.         boolean quoted = false;

  165.         for (int i = 0; i < pattern.length(); ++i) {
  166.             char thisChar = pattern.charAt(i);
  167.             if (quoted) {
  168.                 if (thisChar == '\'') {
  169.                     quoted = false;
  170.                 }
  171.             } else if (thisChar == '\'') {
  172.                 quoted = true;
  173.             } else if (thisChar >= 'a' && thisChar <= 'z' || thisChar >= 'A' && thisChar <= 'Z') {
  174.                 final int index = fromChars.indexOf(thisChar);
  175.                 if (index == -1) {
  176.                     throw new IllegalArgumentException("Illegal pattern character '" + thisChar + "'");
  177.                 }
  178.                 thisChar = toChars.charAt(index);
  179.             }
  180.             converted.append(thisChar);
  181.         }

  182.         if (quoted) {
  183.             throw new IllegalArgumentException("Unfinished quote in pattern");
  184.         }

  185.         return converted.toString();
  186.     }

  187.     /**
  188.      * Tests whether date formatting is lenient.
  189.      *
  190.      * @return true if the {@code DateFormat} used for formatting is lenient
  191.      * @see java.text.DateFormat#isLenient()
  192.      */
  193.     public boolean isLenient() {
  194.         return isLenient;
  195.     }

  196.     /**
  197.      * Convert the specified locale-sensitive input object into an output object of the specified type.
  198.      *
  199.      * @param value   The input object to be converted
  200.      * @param pattern The pattern is used for the conversion
  201.      * @return the converted Date value
  202.      * @throws ConversionException if conversion cannot be performed successfully
  203.      * @throws ParseException      if an error occurs parsing
  204.      */
  205.     @Override
  206.     protected D parse(final Object value, String pattern) throws ParseException {
  207.         // Handle Date
  208.         if (value instanceof Date) {
  209.             return (D) value;
  210.         }

  211.         // Handle Calendar
  212.         if (value instanceof Calendar) {
  213.             return (D) ((Calendar) value).getTime();
  214.         }

  215.         if (localizedPattern) {
  216.             pattern = convertLocalizedPattern(pattern, locale);
  217.         }

  218.         // Create Formatter - use default if pattern is null
  219.         final DateFormat formatter = pattern == null ? DateFormat.getDateInstance(DateFormat.SHORT, locale) : new SimpleDateFormat(pattern, locale);
  220.         formatter.setLenient(isLenient);

  221.         // Parse the Date
  222.         final ParsePosition pos = new ParsePosition(0);
  223.         final String strValue = value.toString();
  224.         final Object parsedValue = formatter.parseObject(strValue, pos);
  225.         if (pos.getErrorIndex() > -1) {
  226.             throw ConversionException.format("Error parsing date '%s' at position = %s", value, pos.getErrorIndex());
  227.         }
  228.         if (pos.getIndex() < strValue.length()) {
  229.             throw ConversionException.format("Date '%s' contains unparsed characters from position = %s", value, pos.getIndex());
  230.         }

  231.         return (D) parsedValue;
  232.     }

  233. }