[jOOQ/jOOQ#8736] Avoid NumberFormatException in Convert

This commit makes `NumberFormatException`s less likely to happen in
`Convert.ConvertAll#from(Object)` by first attempting to parse the
string representation of the value using `Ints#tryParse()` or
`Longs#tryParse()` (as appropriate). If that fails the conversion will
still try to use `BigDecimal(String)`, as that also allows parsing
numbers with fractions and / or exponent.

Note that this change required moving `tryParseInt()` and
`tryParseLong()` from `Tools` to the new public classes `Ints` and
`Longs` which have the license header from the corresponding
Guava sources, where the methods were copied from.
This commit is contained in:
Knut Wannheden 2019-06-07 14:52:39 +02:00
parent dce1a6a2b9
commit ae13be078d
7 changed files with 184 additions and 96 deletions

View File

@ -80,6 +80,7 @@ import org.jooq.conf.SettingsTools;
import org.jooq.conf.StatementType;
import org.jooq.exception.ControlFlowSignal;
import org.jooq.exception.DetachedException;
import org.jooq.tools.Ints;
import org.jooq.tools.JooqLogger;
/**
@ -160,7 +161,7 @@ abstract class AbstractQuery extends AbstractQueryPart implements Query {
@SuppressWarnings("deprecation")
@Override
public Query bind(String param, Object value) {
Integer index = Tools.tryParseInt(param);
Integer index = Ints.tryParse(param);
if (index != null)
return bind(index, value);

View File

@ -172,6 +172,7 @@ import org.jooq.exception.MappingException;
import org.jooq.exception.SQLDialectNotSupportedException;
import org.jooq.tools.Convert;
import org.jooq.tools.JooqLogger;
import org.jooq.tools.Longs;
import org.jooq.tools.StringUtils;
import org.jooq.tools.jdbc.JDBCUtils;
import org.jooq.tools.jdbc.MockArray;
@ -443,7 +444,7 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
private static final long parse(Class<? extends java.util.Date> type, String date) throws SQLException {
// Try reading a plain number first
Long number = Tools.tryParseLong(date);
Long number = Longs.tryParse(date);
if (number != null)
return number;

View File

@ -255,6 +255,7 @@ import org.jooq.exception.NoDataFoundException;
import org.jooq.exception.TooManyRowsException;
import org.jooq.impl.ResultsImpl.ResultOrRowsImpl;
import org.jooq.impl.Tools.Cache.CachedOperation;
import org.jooq.tools.Ints;
import org.jooq.tools.JooqLogger;
import org.jooq.tools.StringUtils;
import org.jooq.tools.jdbc.JDBCUtils;
@ -1752,91 +1753,6 @@ final class Tools {
return result;
}
static Integer tryParseInt(String string) {
return tryParseInt(string, 0, string.length());
}
// adapted from com.google.common.primitives.Longs#tryParse()
private static Integer tryParseInt(String string, int begin, int end) {
if (string == null || end - begin < 1)
return null;
int radix = 10;
char firstChar = string.charAt(begin);
boolean negative = firstChar == '-';
int index = negative || firstChar == '+' ? begin + 1 : begin;
if (index == end)
return null;
int digit = Character.digit(string.charAt(index++), 10);
if (digit < 0 || digit >= radix)
return null;
int accum = -digit;
int cap = Integer.MIN_VALUE / radix;
while (index < end) {
digit = Character.digit(string.charAt(index++), 10);
if (digit < 0 || digit >= radix || accum < cap)
return null;
accum *= radix;
if (accum < Integer.MIN_VALUE + digit)
return null;
accum -= digit;
}
if (negative)
return accum;
else if (accum == Integer.MIN_VALUE)
return null;
else
return -accum;
}
// adapted from com.google.common.primitives.Longs#tryParse()
static Long tryParseLong(String string) {
if (string == null || string.isEmpty())
return null;
int radix = 10;
char firstChar = string.charAt(0);
boolean negative = firstChar == '-';
int index = negative || firstChar == '+' ? 1 : 0;
int length = string.length();
if (index == length)
return null;
int digit = Character.digit(string.charAt(index++), 10);
if (digit < 0 || digit >= radix)
return null;
long accum = -digit;
long cap = Long.MIN_VALUE / radix;
while (index < length) {
digit = Character.digit(string.charAt(index++), 10);
if (digit < 0 || digit >= radix || accum < cap)
return null;
accum *= radix;
if (accum < Long.MIN_VALUE + digit)
return null;
accum -= digit;
}
if (negative)
return accum;
else if (accum == Long.MIN_VALUE)
return null;
else
return -accum;
}
static final <T> List<Field<T>> inline(T[] values) {
if (values == null)
return new ArrayList<Field<T>>();
@ -2514,7 +2430,7 @@ final class Tools {
int end = i;
// Try getting the {numbered placeholder}
Integer index = tryParseInt(sql, start, end);
Integer index = Ints.tryParse(sql, start, end);
if (index != null) {
QueryPart substitute = substitutes.get(index);
render.visit(substitute);

View File

@ -580,7 +580,9 @@ public final class Convert {
return (U) (((Boolean) from) ? Byte.valueOf((byte) 1) : Byte.valueOf((byte) 0));
try {
return (U) Byte.valueOf(new BigDecimal(from.toString().trim()).byteValue());
String fromString = from.toString().trim();
Integer asInt = Ints.tryParse(fromString);
return (U) Byte.valueOf(asInt != null ? asInt.byteValue() : new BigDecimal(fromString).byteValue());
}
catch (NumberFormatException e) {
return Reflect.initValue(toClass);
@ -594,7 +596,9 @@ public final class Convert {
return (U) (((Boolean) from) ? Short.valueOf((short) 1) : Short.valueOf((short) 0));
try {
return (U) Short.valueOf(new BigDecimal(from.toString().trim()).shortValue());
String fromString = from.toString().trim();
Integer asInt = Ints.tryParse(fromString);
return (U) Short.valueOf(asInt != null ? asInt.shortValue() : new BigDecimal(fromString).shortValue());
}
catch (NumberFormatException e) {
return Reflect.initValue(toClass);
@ -608,7 +612,9 @@ public final class Convert {
return (U) (((Boolean) from) ? Integer.valueOf(1) : Integer.valueOf(0));
try {
return (U) Integer.valueOf(new BigDecimal(from.toString().trim()).intValue());
String fromString = from.toString().trim();
Integer asInt = Ints.tryParse(fromString);
return (U) Integer.valueOf(asInt != null ? asInt.intValue() : new BigDecimal(fromString).intValue());
}
catch (NumberFormatException e) {
return Reflect.initValue(toClass);
@ -632,7 +638,9 @@ public final class Convert {
try {
return (U) Long.valueOf(new BigDecimal(from.toString().trim()).longValue());
String fromString = from.toString().trim();
Long asLong = Longs.tryParse(fromString);
return (U) Long.valueOf(asLong != null ? asLong.longValue() : new BigDecimal(fromString).longValue());
}
catch (NumberFormatException e) {
return Reflect.initValue(toClass);
@ -648,7 +656,9 @@ public final class Convert {
if (wrapperFrom == Boolean.class)
return (U) (((Boolean) from) ? ubyte(1) : ubyte(0));
return (U) ubyte(new BigDecimal(from.toString().trim()).shortValue());
String fromString = from.toString().trim();
Integer asInt = Ints.tryParse(fromString);
return (U) ubyte(asInt != null ? asInt.shortValue() : new BigDecimal(fromString).shortValue());
}
catch (NumberFormatException e) {
return null;
@ -662,7 +672,9 @@ public final class Convert {
if (wrapperFrom == Boolean.class)
return (U) (((Boolean) from) ? ushort(1) : ushort(0));
return (U) ushort(new BigDecimal(from.toString().trim()).intValue());
String fromString = from.toString().trim();
Integer asInt = Ints.tryParse(fromString);
return (U) ushort(asInt != null ? asInt.intValue() : new BigDecimal(fromString).intValue());
}
catch (NumberFormatException e) {
return null;
@ -676,7 +688,9 @@ public final class Convert {
if (wrapperFrom == Boolean.class)
return (U) (((Boolean) from) ? uint(1) : uint(0));
return (U) uint(new BigDecimal(from.toString().trim()).longValue());
String fromString = from.toString().trim();
Long asLong = Longs.tryParse(fromString);
return (U) uint(asLong != null ? asLong.longValue() : new BigDecimal(fromString).longValue());
}
catch (NumberFormatException e) {
return null;
@ -695,7 +709,10 @@ public final class Convert {
try {
return (U) ulong(new BigDecimal(from.toString().trim()).toBigInteger().toString());
String fromString = from.toString().trim();
// tryParse() will return null in case of overflow
Long asLong = Longs.tryParse(fromString);
return asLong != null ? (U) ulong(asLong.longValue()) : (U) ulong(new BigDecimal(fromString).toBigInteger());
}
catch (NumberFormatException e) {
return null;

View File

@ -0,0 +1,78 @@
/*
* Copyright (C) 2008 The Guava Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.jooq.tools;
import java.util.Arrays;
/**
* Static utility methods pertaining to {@code int} primitives, that are not already found in either
* {@link Integer} or {@link Arrays}.
*
* <p>See the Guava User Guide article on <a
* href="https://github.com/google/guava/wiki/PrimitivesExplained">primitive utilities</a>.
*
* @author Kevin Bourrillion
* @since 1.0
*/
public final class Ints {
private Ints() {}
public static Integer tryParse(String string) {
return tryParse(string, 0, string.length());
}
// adapted from com.google.common.primitives.Longs#tryParse()
// modified for performance and to allow parsing numbers with leading '+'
public static Integer tryParse(String string, int begin, int end) {
if (begin < 0 || end > string.length() || end - begin < 1)
return null;
int radix = 10;
char firstChar = string.charAt(begin);
boolean negative = firstChar == '-';
int index = negative || firstChar == '+' ? begin + 1 : begin;
if (index == end)
return null;
int digit = Character.digit(string.charAt(index++), 10);
if (digit < 0 || digit >= radix)
return null;
int accum = -digit;
int cap = Integer.MIN_VALUE / radix;
while (index < end) {
digit = Character.digit(string.charAt(index++), 10);
if (digit < 0 || digit >= radix || accum < cap)
return null;
accum *= radix;
if (accum < Integer.MIN_VALUE + digit)
return null;
accum -= digit;
}
if (negative)
return accum;
else if (accum == Integer.MIN_VALUE)
return null;
else
return -accum;
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (C) 2008 The Guava Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.jooq.tools;
import java.util.Arrays;
/**
* Static utility methods pertaining to {@code long} primitives, that are not already found in
* either {@link Long} or {@link Arrays}.
*
* <p>See the Guava User Guide article on <a
* href="https://github.com/google/guava/wiki/PrimitivesExplained">primitive utilities</a>.
*
* @author Kevin Bourrillion
* @since 1.0
*/
public final class Longs {
private Longs() {}
// adapted from com.google.common.primitives.Longs#tryParse()
// modified for performance and to allow parsing numbers with leading '+'
public static Long tryParse(String string) {
if (string.isEmpty())
return null;
int radix = 10;
char firstChar = string.charAt(0);
boolean negative = firstChar == '-';
int index = negative || firstChar == '+' ? 1 : 0;
int length = string.length();
if (index == length)
return null;
int digit = Character.digit(string.charAt(index++), 10);
if (digit < 0 || digit >= radix)
return null;
long accum = -digit;
long cap = Long.MIN_VALUE / radix;
while (index < length) {
digit = Character.digit(string.charAt(index++), 10);
if (digit < 0 || digit >= radix || accum < cap)
return null;
accum *= radix;
if (accum < Long.MIN_VALUE + digit)
return null;
accum -= digit;
}
if (negative)
return accum;
else if (accum == Long.MIN_VALUE)
return null;
else
return -accum;
}
}

View File

@ -1424,4 +1424,5 @@ public final class StringUtils {
String[] result = new String[matchList.size()];
return matchList.toArray(result);
}
}