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 */ 017package org.apache.commons.beanutils2.converters; 018 019import java.math.BigDecimal; 020import java.math.BigInteger; 021import java.text.DecimalFormat; 022import java.text.DecimalFormatSymbols; 023import java.text.NumberFormat; 024import java.text.ParsePosition; 025import java.util.Calendar; 026import java.util.Date; 027import java.util.Locale; 028 029import org.apache.commons.beanutils2.ConversionException; 030 031/** 032 * {@link org.apache.commons.beanutils2.Converter} implementation that handles conversion to and from <strong>java.lang.Number</strong> objects. 033 * <p> 034 * This implementation handles conversion for the following {@link Number} types. 035 * <ul> 036 * <li>{@link Byte}</li> 037 * <li>{@link Short}</li> 038 * <li>{@link Integer}</li> 039 * <li>{@link Long}</li> 040 * <li>{@link Float}</li> 041 * <li>{@link Double}</li> 042 * <li>{@link java.math.BigDecimal}</li> 043 * <li>{@link java.math.BigInteger}</li> 044 * </ul> 045 * 046 * <h2>String Conversions (to and from)</h2> This class provides a number of ways in which number conversions to/from Strings can be achieved: 047 * <ul> 048 * <li>Using the default format for the default Locale, configure using: 049 * <ul> 050 * <li>{@code setUseLocaleFormat(true)}</li> 051 * </ul> 052 * </li> 053 * <li>Using the default format for a specified Locale, configure using: 054 * <ul> 055 * <li>{@code setLocale(Locale)}</li> 056 * </ul> 057 * </li> 058 * <li>Using a specified pattern for the default Locale, configure using: 059 * <ul> 060 * <li>{@code setPattern(String)}</li> 061 * </ul> 062 * </li> 063 * <li>Using a specified pattern for a specified Locale, configure using: 064 * <ul> 065 * <li>{@code setPattern(String)}</li> 066 * <li>{@code setLocale(Locale)}</li> 067 * </ul> 068 * </li> 069 * <li>If none of the above are configured the {@code toNumber(String)} method is used to convert from String to Number and the Number's {@code toString()} 070 * method used to convert from Number to String.</li> 071 * </ul> 072 * 073 * <p> 074 * <strong>N.B.</strong>Patterns can only be specified using the <em>standard</em> pattern characters and NOT in <em>localized</em> form (see 075 * {@link java.text.DecimalFormat}). For example to cater for number styles used in Germany such as {@code 0.000,00} the pattern is specified in the normal form 076 * {@code 0,000.00</code> and the locale set to <code>Locale.GERMANY} 077 * 078 * @param <N> The default value type. 079 * @since 1.8.0 080 */ 081public abstract class NumberConverter<N extends Number> extends AbstractConverter<N> { 082 083 private static final Integer ZERO = Integer.valueOf(0); 084 private static final Integer ONE = Integer.valueOf(1); 085 086 private String pattern; 087 private final boolean allowDecimals; 088 private boolean useLocaleFormat; 089 private Locale locale; 090 091 /** 092 * Constructs a <strong>java.lang.Number</strong> <em>Converter</em> that throws a {@code ConversionException} if a error occurs. 093 * 094 * @param allowDecimals Indicates whether decimals are allowed 095 */ 096 public NumberConverter(final boolean allowDecimals) { 097 this.allowDecimals = allowDecimals; 098 } 099 100 /** 101 * Constructs a {@link Number} <em>Converter</em> that returns a default value if an error occurs. 102 * 103 * @param allowDecimals Indicates whether decimals are allowed 104 * @param defaultValue The default value to be returned 105 */ 106 public NumberConverter(final boolean allowDecimals, final N defaultValue) { 107 this.allowDecimals = allowDecimals; 108 setDefaultValue(defaultValue); 109 } 110 111 /** 112 * Convert an input Number object into a String. 113 * 114 * @param value The input value to be converted 115 * @return the converted String value. 116 * @throws IllegalArgumentException if an error occurs converting to a String 117 */ 118 @Override 119 protected String convertToString(final Object value) { 120 121 String result = null; 122 if (useLocaleFormat && value instanceof Number) { 123 final NumberFormat format = getFormat(); 124 format.setGroupingUsed(false); 125 result = format.format(value); 126 if (log().isDebugEnabled()) { 127 log().debug(" Converted to String using format '" + result + "'"); 128 } 129 130 } else { 131 result = value.toString(); 132 if (log().isDebugEnabled()) { 133 log().debug(" Converted to String using toString() '" + result + "'"); 134 } 135 } 136 return result; 137 138 } 139 140 /** 141 * Convert the input object into a Number object of the specified type. 142 * 143 * @param <T> Target type of the conversion. 144 * @param targetType Data type to which this value should be converted. 145 * @param value The input value to be converted. 146 * @return The converted value. 147 * @throws Throwable if an error occurs converting to the specified type 148 */ 149 @Override 150 protected <T> T convertToType(final Class<T> targetType, final Object value) throws Throwable { 151 final Class<?> sourceType = value.getClass(); 152 // Handle Number 153 if (value instanceof Number) { 154 return toNumber(sourceType, targetType, (Number) value); 155 } 156 157 // Handle Boolean 158 if (value instanceof Boolean) { 159 return toNumber(sourceType, targetType, ((Boolean) value).booleanValue() ? ONE : ZERO); 160 } 161 162 // Handle Date --> Long 163 if (value instanceof Date && Long.class.equals(targetType)) { 164 return targetType.cast(Long.valueOf(((Date) value).getTime())); 165 } 166 167 // Handle Calendar --> Long 168 if (value instanceof Calendar && Long.class.equals(targetType)) { 169 return targetType.cast(Long.valueOf(((Calendar) value).getTime().getTime())); 170 } 171 172 // Convert all other types to String & handle 173 final String stringValue = toTrim(value); 174 if (stringValue.isEmpty()) { 175 return handleMissing(targetType); 176 } 177 178 // Convert/Parse a String 179 Number number = null; 180 if (useLocaleFormat) { 181 final NumberFormat format = getFormat(); 182 number = parse(sourceType, targetType, stringValue, format); 183 } else { 184 if (log().isDebugEnabled()) { 185 log().debug(" No NumberFormat, using default conversion"); 186 } 187 number = toNumber(sourceType, targetType, stringValue); 188 } 189 190 // Ensure the correct number type is returned 191 return toNumber(sourceType, targetType, number); 192 } 193 194 /** 195 * Gets a NumberFormat to use for Conversion. 196 * 197 * @return The NumberFormat. 198 */ 199 private NumberFormat getFormat() { 200 NumberFormat format = null; 201 if (pattern != null) { 202 if (locale == null) { 203 if (log().isDebugEnabled()) { 204 log().debug(" Using pattern '" + pattern + "'"); 205 } 206 format = new DecimalFormat(pattern); 207 } else { 208 if (log().isDebugEnabled()) { 209 log().debug(" Using pattern '" + pattern + "'" + " with Locale[" + locale + "]"); 210 } 211 final DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale); 212 format = new DecimalFormat(pattern, symbols); 213 } 214 } else if (locale == null) { 215 if (log().isDebugEnabled()) { 216 log().debug(" Using default Locale format"); 217 } 218 format = NumberFormat.getInstance(); 219 } else { 220 if (log().isDebugEnabled()) { 221 log().debug(" Using Locale[" + locale + "] format"); 222 } 223 format = NumberFormat.getInstance(locale); 224 } 225 if (!allowDecimals) { 226 format.setParseIntegerOnly(true); 227 } 228 return format; 229 } 230 231 /** 232 * Gets the Locale for the <em>Converter</em> (or {@code null} if none specified). 233 * 234 * @return The locale to use for conversion 235 */ 236 public Locale getLocale() { 237 return locale; 238 } 239 240 /** 241 * Gets the number format pattern used to convert Numbers to/from a {@link String} (or {@code null} if none specified). 242 * <p> 243 * See {@link java.text.DecimalFormat} for details of how to specify the pattern. 244 * 245 * @return The format pattern. 246 */ 247 public String getPattern() { 248 return pattern; 249 } 250 251 /** 252 * Gets whether decimals are allowed in the number. 253 * 254 * @return Whether decimals are allowed in the number 255 */ 256 public boolean isAllowDecimals() { 257 return allowDecimals; 258 } 259 260 /** 261 * Convert a String into a {@code Number} object. 262 * 263 * @param sourceType the source type of the conversion 264 * @param targetType The type to convert the value to 265 * @param value The String date value. 266 * @param format The NumberFormat to parse the String value. 267 * @return The converted Number object. 268 * @throws ConversionException if the String cannot be converted. 269 */ 270 private Number parse(final Class<?> sourceType, final Class<?> targetType, final String value, final NumberFormat format) { 271 final ParsePosition pos = new ParsePosition(0); 272 final Number parsedNumber = format.parse(value, pos); 273 if (pos.getErrorIndex() >= 0 || pos.getIndex() != value.length() || parsedNumber == null) { 274 String msg = "Error converting from '" + toString(sourceType) + "' to '" + toString(targetType) + "'"; 275 if (format instanceof DecimalFormat) { 276 msg += " using pattern '" + ((DecimalFormat) format).toPattern() + "'"; 277 } 278 if (locale != null) { 279 msg += " for locale=[" + locale + "]"; 280 } 281 if (log().isDebugEnabled()) { 282 log().debug(" " + msg); 283 } 284 throw new ConversionException(msg); 285 } 286 return parsedNumber; 287 } 288 289 /** 290 * Sets the Locale for the <em>Converter</em>. 291 * 292 * @param locale The locale to use for conversion 293 */ 294 public void setLocale(final Locale locale) { 295 this.locale = locale; 296 setUseLocaleFormat(true); 297 } 298 299 /** 300 * Sets a number format pattern to use to convert Numbers to/from a {@link String}. 301 * <p> 302 * See {@link java.text.DecimalFormat} for details of how to specify the pattern. 303 * 304 * @param pattern The format pattern. 305 */ 306 public void setPattern(final String pattern) { 307 this.pattern = pattern; 308 setUseLocaleFormat(true); 309 } 310 311 /** 312 * Sets whether a format should be used to convert the Number. 313 * 314 * @param useLocaleFormat {@code true} if a number format should be used. 315 */ 316 public void setUseLocaleFormat(final boolean useLocaleFormat) { 317 this.useLocaleFormat = useLocaleFormat; 318 } 319 320 /** 321 * Default String to Number conversion. 322 * <p> 323 * This method handles conversion from a String to the following types: 324 * <ul> 325 * <li>{@link Byte}</li> 326 * <li>{@link Short}</li> 327 * <li>{@link Integer}</li> 328 * <li>{@link Long}</li> 329 * <li>{@link Float}</li> 330 * <li>{@link Double}</li> 331 * <li>{@link java.math.BigDecimal}</li> 332 * <li>{@link java.math.BigInteger}</li> 333 * </ul> 334 * 335 * @param sourceType The type being converted from 336 * @param targetType The Number type to convert to 337 * @param value The String value to convert. 338 * @return The converted Number value. 339 */ 340 private Number toNumber(final Class<?> sourceType, final Class<?> targetType, final String value) { 341 // Byte 342 if (targetType.equals(Byte.class)) { 343 return Byte.valueOf(value); 344 } 345 346 // Short 347 if (targetType.equals(Short.class)) { 348 return Short.valueOf(value); 349 } 350 351 // Integer 352 if (targetType.equals(Integer.class)) { 353 return Integer.valueOf(value); 354 } 355 356 // Long 357 if (targetType.equals(Long.class)) { 358 return Long.valueOf(value); 359 } 360 361 // Float 362 if (targetType.equals(Float.class)) { 363 return Float.valueOf(value); 364 } 365 366 // Double 367 if (targetType.equals(Double.class)) { 368 return Double.valueOf(value); 369 } 370 371 // BigDecimal 372 if (targetType.equals(BigDecimal.class)) { 373 return new BigDecimal(value); 374 } 375 376 // BigInteger 377 if (targetType.equals(BigInteger.class)) { 378 return new BigInteger(value); 379 } 380 381 final String msg = toString(getClass()) + " cannot handle conversion from '" + toString(sourceType) + "' to '" + toString(targetType) + "'"; 382 if (log().isWarnEnabled()) { 383 log().warn(" " + msg); 384 } 385 throw new ConversionException(msg); 386 } 387 388 /** 389 * Convert any Number object to the specified type for this <em>Converter</em>. 390 * <p> 391 * This method handles conversion to the following types: 392 * <ul> 393 * <li>{@link Byte}</li> 394 * <li>{@link Short}</li> 395 * <li>{@link Integer}</li> 396 * <li>{@link Long}</li> 397 * <li>{@link Float}</li> 398 * <li>{@link Double}</li> 399 * <li>{@link java.math.BigDecimal}</li> 400 * <li>{@link java.math.BigInteger}</li> 401 * </ul> 402 * 403 * @param sourceType The type being converted from 404 * @param targetType The Number type to convert to 405 * @param value The Number to convert. 406 * @return The converted value. 407 */ 408 private <T> T toNumber(final Class<?> sourceType, final Class<T> targetType, final Number value) { 409 // Correct Number type already 410 if (targetType.equals(value.getClass())) { 411 return targetType.cast(value); 412 } 413 414 // Byte 415 if (targetType.equals(Byte.class)) { 416 final long longValue = value.longValue(); 417 if (longValue > Byte.MAX_VALUE) { 418 throw ConversionException.format("%s value '%s' is too large for %s", toString(sourceType), value, toString(targetType)); 419 } 420 if (longValue < Byte.MIN_VALUE) { 421 throw ConversionException.format("%s value '%s' is too small %s", toString(sourceType), value, toString(targetType)); 422 } 423 return targetType.cast(Byte.valueOf(value.byteValue())); 424 } 425 426 // Short 427 if (targetType.equals(Short.class)) { 428 final long longValue = value.longValue(); 429 if (longValue > Short.MAX_VALUE) { 430 throw ConversionException.format("%s value '%s' is too large for %s", toString(sourceType), value, toString(targetType)); 431 } 432 if (longValue < Short.MIN_VALUE) { 433 throw ConversionException.format("%s value '%s' is too small %s", toString(sourceType), value, toString(targetType)); 434 } 435 return targetType.cast(Short.valueOf(value.shortValue())); 436 } 437 438 // Integer 439 if (targetType.equals(Integer.class)) { 440 final long longValue = value.longValue(); 441 if (longValue > Integer.MAX_VALUE) { 442 throw ConversionException.format("%s value '%s' is too large for %s", toString(sourceType), value, toString(targetType)); 443 } 444 if (longValue < Integer.MIN_VALUE) { 445 throw ConversionException.format("%s value '%s' is too small %s", toString(sourceType), value, toString(targetType)); 446 } 447 return targetType.cast(Integer.valueOf(value.intValue())); 448 } 449 450 // Long 451 if (targetType.equals(Long.class)) { 452 return targetType.cast(Long.valueOf(value.longValue())); 453 } 454 455 // Float 456 if (targetType.equals(Float.class)) { 457 if (value.doubleValue() > Float.MAX_VALUE) { 458 throw ConversionException.format("%s value '%s' is too large for %s", toString(sourceType), value, toString(targetType)); 459 } 460 return targetType.cast(Float.valueOf(value.floatValue())); 461 } 462 463 // Double 464 if (targetType.equals(Double.class)) { 465 return targetType.cast(Double.valueOf(value.doubleValue())); 466 } 467 468 // BigDecimal 469 if (targetType.equals(BigDecimal.class)) { 470 if (value instanceof Float || value instanceof Double) { 471 return targetType.cast(new BigDecimal(value.toString())); 472 } 473 if (value instanceof BigInteger) { 474 return targetType.cast(new BigDecimal((BigInteger) value)); 475 } 476 if (value instanceof BigDecimal) { 477 return targetType.cast(new BigDecimal(value.toString())); 478 } 479 return targetType.cast(BigDecimal.valueOf(value.longValue())); 480 } 481 482 // BigInteger 483 if (targetType.equals(BigInteger.class)) { 484 if (value instanceof BigDecimal) { 485 return targetType.cast(((BigDecimal) value).toBigInteger()); 486 } 487 return targetType.cast(BigInteger.valueOf(value.longValue())); 488 } 489 490 final String msg = toString(getClass()) + " cannot handle conversion to '" + toString(targetType) + "'"; 491 if (log().isWarnEnabled()) { 492 log().warn(" " + msg); 493 } 494 throw new ConversionException(msg); 495 } 496 497 /** 498 * Provide a String representation of this number converter. 499 * 500 * @return A String representation of this number converter 501 */ 502 @Override 503 public String toString() { 504 final StringBuilder buffer = new StringBuilder(); 505 buffer.append(toString(getClass())); 506 buffer.append("[UseDefault="); 507 buffer.append(isUseDefault()); 508 buffer.append(", UseLocaleFormat="); 509 buffer.append(useLocaleFormat); 510 if (pattern != null) { 511 buffer.append(", Pattern="); 512 buffer.append(pattern); 513 } 514 if (locale != null) { 515 buffer.append(", Locale="); 516 buffer.append(locale); 517 } 518 buffer.append(']'); 519 return buffer.toString(); 520 } 521 522}