| /******************************************************************************* |
| * Copyright (c) 2007, 2008 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| * Michael Scharf - bug 240562 |
| * Matt Carter - bug 180392 |
| ******************************************************************************/ |
| |
| package org.eclipse.core.databinding.conversion; |
| |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.math.BigDecimal; |
| import java.math.BigInteger; |
| |
| import org.eclipse.core.internal.databinding.conversion.StringToNumberParser; |
| import org.eclipse.core.internal.databinding.conversion.StringToNumberParser.ParseResult; |
| import org.eclipse.core.internal.databinding.validation.NumberFormatConverter; |
| |
| import com.ibm.icu.text.NumberFormat; |
| |
| /** |
| * Converts a String to a Number using <code>NumberFormat.parse(...)</code>. |
| * This class is thread safe. |
| * |
| * @since 1.0 |
| */ |
| public class StringToNumberConverter extends NumberFormatConverter { |
| private Class toType; |
| /** |
| * NumberFormat instance to use for conversion. Access must be synchronized. |
| */ |
| private NumberFormat numberFormat; |
| |
| /** |
| * Minimum possible value for the type. Can be <code>null</code> as |
| * BigInteger doesn't have bounds. |
| */ |
| private final Number min; |
| /** |
| * Maximum possible value for the type. Can be <code>null</code> as |
| * BigInteger doesn't have bounds. |
| */ |
| private final Number max; |
| |
| /** |
| * The boxed type of the toType; |
| */ |
| private final Class boxedType; |
| |
| private static final Integer MIN_INTEGER = new Integer(Integer.MIN_VALUE); |
| private static final Integer MAX_INTEGER = new Integer(Integer.MAX_VALUE); |
| |
| // This code looks deceptive, but we can't use Double.MIN_VALUE because it |
| // is actually the smallest *positive* number. |
| private static final Double MIN_DOUBLE = new Double(-Double.MAX_VALUE); |
| private static final Double MAX_DOUBLE = new Double(Double.MAX_VALUE); |
| |
| private static final Long MIN_LONG = new Long(Long.MIN_VALUE); |
| private static final Long MAX_LONG = new Long(Long.MAX_VALUE); |
| |
| // This code looks deceptive, but we can't use Float.MIN_VALUE because it is |
| // actually the smallest *positive* number. |
| private static final Float MIN_FLOAT = new Float(-Float.MAX_VALUE); |
| private static final Float MAX_FLOAT = new Float(Float.MAX_VALUE); |
| |
| private static final Short MIN_SHORT = new Short(Short.MIN_VALUE); |
| private static final Short MAX_SHORT = new Short(Short.MAX_VALUE); |
| |
| private static final Byte MIN_BYTE = new Byte(Byte.MIN_VALUE); |
| private static final Byte MAX_BYTE = new Byte(Byte.MAX_VALUE); |
| |
| static Class icuBigDecimal = null; |
| static Method icuBigDecimalScale = null; |
| static Method icuBigDecimalUnscaledValue = null; |
| |
| { |
| /* |
| * If the full ICU4J library is available, we use the ICU BigDecimal |
| * class to support proper formatting and parsing of java.math.BigDecimal. |
| * |
| * The version of ICU NumberFormat (DecimalFormat) included in eclipse excludes |
| * support for java.math.BigDecimal, and if used falls back to converting as |
| * an unknown Number type via doubleValue(), which is undesirable. |
| * |
| * See Bug #180392. |
| */ |
| try { |
| icuBigDecimal = Class.forName("com.ibm.icu.math.BigDecimal"); //$NON-NLS-1$ |
| icuBigDecimalScale = icuBigDecimal.getMethod("scale", null); //$NON-NLS-1$ |
| icuBigDecimalUnscaledValue = icuBigDecimal.getMethod("unscaledValue", null); //$NON-NLS-1$ |
| /* System.out.println("DEBUG: Full ICU4J support state: icuBigDecimal="+ //$NON-NLS-1$ |
| (icuBigDecimal != null)+", icuBigDecimalScale="+(icuBigDecimalScale != null)+ //$NON-NLS-1$ |
| ", icuBigDecimalUnscaledValue="+(icuBigDecimalUnscaledValue != null)); //$NON-NLS-1$ */ |
| } |
| catch(ClassNotFoundException e) {} |
| catch(NoSuchMethodException e) {} |
| } |
| /** |
| * @param numberFormat |
| * @param toType |
| * @param min |
| * minimum possible value for the type, can be <code>null</code> |
| * as BigInteger doesn't have bounds |
| * @param max |
| * maximum possible value for the type, can be <code>null</code> |
| * as BigInteger doesn't have bounds |
| * @param boxedType |
| * a convenience that allows for the checking against one type |
| * rather than boxed and unboxed types |
| */ |
| private StringToNumberConverter(NumberFormat numberFormat, Class toType, |
| Number min, Number max, Class boxedType) { |
| super(String.class, toType, numberFormat); |
| |
| this.toType = toType; |
| this.numberFormat = numberFormat; |
| this.min = min; |
| this.max = max; |
| this.boxedType = boxedType; |
| } |
| |
| /** |
| * Converts the provided <code>fromObject</code> to the requested |
| * {@link #getToType() to type}. |
| * |
| * @see org.eclipse.core.databinding.conversion.IConverter#convert(java.lang.Object) |
| * @throws IllegalArgumentException |
| * if the value isn't in the format required by the NumberFormat |
| * or the value is out of range for the |
| * {@link #getToType() to type}. |
| * @throws IllegalArgumentException |
| * if conversion was not possible |
| */ |
| public Object convert(Object fromObject) { |
| ParseResult result = StringToNumberParser.parse(fromObject, |
| numberFormat, toType.isPrimitive()); |
| |
| if (result.getPosition() != null) { |
| // this shouldn't happen in the pipeline as validation should catch |
| // it but anyone can call convert so we should return a properly |
| // formatted message in an exception |
| throw new IllegalArgumentException(StringToNumberParser |
| .createParseErrorMessage((String) fromObject, result |
| .getPosition())); |
| } else if (result.getNumber() == null) { |
| // if an error didn't occur and the number is null then it's a boxed |
| // type and null should be returned |
| return null; |
| } |
| |
| /* |
| * Technically the checks for ranges aren't needed here because the |
| * validator should have validated this already but we shouldn't assume |
| * this has occurred. |
| */ |
| if (Integer.class.equals(boxedType)) { |
| if (StringToNumberParser.inIntegerRange(result.getNumber())) { |
| return new Integer(result.getNumber().intValue()); |
| } |
| } else if (Double.class.equals(boxedType)) { |
| if (StringToNumberParser.inDoubleRange(result.getNumber())) { |
| return new Double(result.getNumber().doubleValue()); |
| } |
| } else if (Long.class.equals(boxedType)) { |
| if (StringToNumberParser.inLongRange(result.getNumber())) { |
| return new Long(result.getNumber().longValue()); |
| } |
| } else if (Float.class.equals(boxedType)) { |
| if (StringToNumberParser.inFloatRange(result.getNumber())) { |
| return new Float(result.getNumber().floatValue()); |
| } |
| } else if (BigInteger.class.equals(boxedType)) { |
| Number n = result.getNumber(); |
| if(n instanceof Long) |
| return BigInteger.valueOf(n.longValue()); |
| else if(n instanceof BigInteger) |
| return n; |
| else if(n instanceof BigDecimal) |
| return ((BigDecimal) n).toBigInteger(); |
| else |
| return new BigDecimal(n.doubleValue()).toBigInteger(); |
| } else if (BigDecimal.class.equals(boxedType)) { |
| Number n = result.getNumber(); |
| if(n instanceof Long) |
| return BigDecimal.valueOf(n.longValue()); |
| else if(n instanceof BigInteger) |
| return new BigDecimal((BigInteger) n); |
| else if(n instanceof BigDecimal) |
| return n; |
| else if(icuBigDecimal != null && icuBigDecimal.isInstance(n)) { |
| try { |
| // Get ICU BigDecimal value and use to construct java.math.BigDecimal |
| int scale = ((Integer) icuBigDecimalScale.invoke(n, null)).intValue(); |
| BigInteger unscaledValue = (BigInteger) icuBigDecimalUnscaledValue.invoke(n, null); |
| return new java.math.BigDecimal(unscaledValue, scale); |
| } catch(IllegalAccessException e) { |
| throw new IllegalArgumentException("Error (IllegalAccessException) converting BigDecimal using ICU"); //$NON-NLS-1$ |
| } catch(InvocationTargetException e) { |
| throw new IllegalArgumentException("Error (InvocationTargetException) converting BigDecimal using ICU"); //$NON-NLS-1$ |
| } |
| } else if(n instanceof Double) { |
| BigDecimal bd = new BigDecimal(n.doubleValue()); |
| if(bd.scale() == 0) return bd; |
| throw new IllegalArgumentException("Non-integral Double value returned from NumberFormat " + //$NON-NLS-1$ |
| "which cannot be accurately stored in a BigDecimal due to lost precision. " + //$NON-NLS-1$ |
| "Consider using ICU4J or Java 5 which can properly format and parse these types."); //$NON-NLS-1$ |
| } |
| } else if (Short.class.equals(boxedType)) { |
| if (StringToNumberParser.inShortRange(result.getNumber())) { |
| return new Short(result.getNumber().shortValue()); |
| } |
| } else if (Byte.class.equals(boxedType)) { |
| if (StringToNumberParser.inByteRange(result.getNumber())) { |
| return new Byte(result.getNumber().byteValue()); |
| } |
| } |
| |
| if (min != null && max != null) { |
| throw new IllegalArgumentException(StringToNumberParser |
| .createOutOfRangeMessage(min, max, numberFormat)); |
| } |
| |
| /* |
| * Fail safe. I don't think this could even be thrown but throwing the |
| * exception is better than returning null and hiding the error. |
| */ |
| throw new IllegalArgumentException( |
| "Could not convert [" + fromObject + "] to type [" + toType + "]"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| } |
| |
| /** |
| * @param primitive |
| * <code>true</code> if the convert to type is an int |
| * @return to Integer converter for the default locale |
| */ |
| public static StringToNumberConverter toInteger(boolean primitive) { |
| return toInteger(NumberFormat.getIntegerInstance(), primitive); |
| } |
| |
| /** |
| * @param numberFormat |
| * @param primitive |
| * @return to Integer converter with the provided numberFormat |
| */ |
| public static StringToNumberConverter toInteger(NumberFormat numberFormat, |
| boolean primitive) { |
| return new StringToNumberConverter(numberFormat, |
| (primitive) ? Integer.TYPE : Integer.class, MIN_INTEGER, |
| MAX_INTEGER, Integer.class); |
| } |
| |
| /** |
| * @param primitive |
| * <code>true</code> if the convert to type is a double |
| * @return to Double converter for the default locale |
| */ |
| public static StringToNumberConverter toDouble(boolean primitive) { |
| return toDouble(NumberFormat.getNumberInstance(), primitive); |
| } |
| |
| /** |
| * @param numberFormat |
| * @param primitive |
| * @return to Double converter with the provided numberFormat |
| */ |
| public static StringToNumberConverter toDouble(NumberFormat numberFormat, |
| boolean primitive) { |
| return new StringToNumberConverter(numberFormat, |
| (primitive) ? Double.TYPE : Double.class, MIN_DOUBLE, |
| MAX_DOUBLE, Double.class); |
| } |
| |
| /** |
| * @param primitive |
| * <code>true</code> if the convert to type is a long |
| * @return to Long converter for the default locale |
| */ |
| public static StringToNumberConverter toLong(boolean primitive) { |
| return toLong(NumberFormat.getIntegerInstance(), primitive); |
| } |
| |
| /** |
| * @param numberFormat |
| * @param primitive |
| * @return to Long converter with the provided numberFormat |
| */ |
| public static StringToNumberConverter toLong(NumberFormat numberFormat, |
| boolean primitive) { |
| return new StringToNumberConverter(numberFormat, |
| (primitive) ? Long.TYPE : Long.class, MIN_LONG, MAX_LONG, |
| Long.class); |
| } |
| |
| /** |
| * @param primitive |
| * <code>true</code> if the convert to type is a float |
| * @return to Float converter for the default locale |
| */ |
| public static StringToNumberConverter toFloat(boolean primitive) { |
| return toFloat(NumberFormat.getNumberInstance(), primitive); |
| } |
| |
| /** |
| * @param numberFormat |
| * @param primitive |
| * @return to Float converter with the provided numberFormat |
| */ |
| public static StringToNumberConverter toFloat(NumberFormat numberFormat, |
| boolean primitive) { |
| return new StringToNumberConverter(numberFormat, |
| (primitive) ? Float.TYPE : Float.class, MIN_FLOAT, MAX_FLOAT, |
| Float.class); |
| } |
| |
| /** |
| * @return to BigInteger converter for the default locale |
| */ |
| public static StringToNumberConverter toBigInteger() { |
| return toBigInteger(NumberFormat.getIntegerInstance()); |
| } |
| |
| /** |
| * @param numberFormat |
| * @return to BigInteger converter with the provided numberFormat |
| */ |
| public static StringToNumberConverter toBigInteger(NumberFormat numberFormat) { |
| return new StringToNumberConverter(numberFormat, BigInteger.class, |
| null, null, BigInteger.class); |
| } |
| |
| /** |
| * @return to BigDecimal converter for the default locale |
| * @since 1.2 |
| */ |
| public static StringToNumberConverter toBigDecimal() { |
| return toBigDecimal(NumberFormat.getNumberInstance()); |
| } |
| |
| /** |
| * @param numberFormat |
| * @return to BigDecimal converter with the provided numberFormat |
| * @since 1.2 |
| */ |
| public static StringToNumberConverter toBigDecimal(NumberFormat numberFormat) { |
| return new StringToNumberConverter(numberFormat, BigDecimal.class, |
| null, null, BigDecimal.class); |
| } |
| |
| /** |
| * @param primitive |
| * <code>true</code> if the convert to type is a short |
| * @return to Short converter for the default locale |
| * @since 1.2 |
| */ |
| public static StringToNumberConverter toShort(boolean primitive) { |
| return toShort(NumberFormat.getIntegerInstance(), primitive); |
| } |
| |
| /** |
| * @param numberFormat |
| * @param primitive |
| * @return to Short converter with the provided numberFormat |
| * @since 1.2 |
| */ |
| public static StringToNumberConverter toShort(NumberFormat numberFormat, |
| boolean primitive) { |
| return new StringToNumberConverter(numberFormat, |
| (primitive) ? Short.TYPE : Short.class, MIN_SHORT, |
| MAX_SHORT, Short.class); |
| } |
| |
| /** |
| * @param primitive |
| * <code>true</code> if the convert to type is a byte |
| * @return to Byte converter for the default locale |
| * @since 1.2 |
| */ |
| public static StringToNumberConverter toByte(boolean primitive) { |
| return toByte(NumberFormat.getIntegerInstance(), primitive); |
| } |
| |
| /** |
| * @param numberFormat |
| * @param primitive |
| * @return to Byte converter with the provided numberFormat |
| * @since 1.2 |
| */ |
| public static StringToNumberConverter toByte(NumberFormat numberFormat, |
| boolean primitive) { |
| return new StringToNumberConverter(numberFormat, |
| (primitive) ? Byte.TYPE : Byte.class, MIN_BYTE, |
| MAX_BYTE, Byte.class); |
| } |
| |
| } |