diff --git a/src/main/java/com/fasterxml/jackson/core/JsonParser.java b/src/main/java/com/fasterxml/jackson/core/JsonParser.java index 50c6b0e1a7..f1a9692aff 100644 --- a/src/main/java/com/fasterxml/jackson/core/JsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/JsonParser.java @@ -348,6 +348,17 @@ public enum Feature { */ USE_FAST_DOUBLE_PARSER(false), + /** + * Feature that determines whether to use the built-in Java code for parsing + * BigDecimals and BigIntegerss or to use + * {@link com.fasterxml.jackson.core.io.doubleparser} instead. + *

+ * This setting is disabled by default for backwards compatibility. + * + * @since 2.15 + */ + USE_FAST_BIG_NUMBER_PARSER(false) + ; /** diff --git a/src/main/java/com/fasterxml/jackson/core/StreamReadFeature.java b/src/main/java/com/fasterxml/jackson/core/StreamReadFeature.java index fb1fca0581..69b2dda8f1 100644 --- a/src/main/java/com/fasterxml/jackson/core/StreamReadFeature.java +++ b/src/main/java/com/fasterxml/jackson/core/StreamReadFeature.java @@ -100,7 +100,18 @@ public enum StreamReadFeature * * @since 2.14 */ - USE_FAST_DOUBLE_PARSER(JsonParser.Feature.USE_FAST_DOUBLE_PARSER) + USE_FAST_DOUBLE_PARSER(JsonParser.Feature.USE_FAST_DOUBLE_PARSER), + + /** + * Feature that determines whether to use the built-in Java code for parsing + * BigDecimals and BigIntegerss or to use + * {@link com.fasterxml.jackson.core.io.doubleparser} instead. + *

+ * This setting is disabled by default. + * + * @since 2.15 + */ + USE_FAST_BIG_NUMBER_PARSER(JsonParser.Feature.USE_FAST_BIG_NUMBER_PARSER) ; diff --git a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java index 79c1f3f8ee..a713f1b003 100644 --- a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java +++ b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java @@ -1148,7 +1148,9 @@ protected BigInteger _getBigInteger() { } else if (_numberString == null) { throw new IllegalStateException("cannot get BigInteger from current parser state"); } - _numberBigInt = NumberInput.parseBigInteger(_numberString); + _numberBigInt = NumberInput.parseBigInteger( + _numberString, + isEnabled(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER)); _numberString = null; return _numberBigInt; } @@ -1165,7 +1167,9 @@ protected BigDecimal _getBigDecimal() { } else if (_numberString == null) { throw new IllegalStateException("cannot get BigDecimal from current parser state"); } - _numberBigDecimal = NumberInput.parseBigDecimal(_numberString); + _numberBigDecimal = NumberInput.parseBigDecimal( + _numberString, + isEnabled(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER)); _numberString = null; return _numberBigDecimal; } diff --git a/src/main/java/com/fasterxml/jackson/core/io/BigDecimalParser.java b/src/main/java/com/fasterxml/jackson/core/io/BigDecimalParser.java index 173af8300e..b86502aaf2 100644 --- a/src/main/java/com/fasterxml/jackson/core/io/BigDecimalParser.java +++ b/src/main/java/com/fasterxml/jackson/core/io/BigDecimalParser.java @@ -1,5 +1,7 @@ package com.fasterxml.jackson.core.io; +import ch.randelshofer.fastdoubleparser.JavaBigDecimalParser; + import java.math.BigDecimal; import java.util.Arrays; @@ -21,7 +23,7 @@ */ public final class BigDecimalParser { - private final static int MAX_CHARS_TO_REPORT = 1000; + final static int MAX_CHARS_TO_REPORT = 1000; private BigDecimalParser() {} @@ -60,6 +62,29 @@ public static BigDecimal parse(char[] chars) { return parse(chars, 0, chars.length); } + public static BigDecimal parseWithFastParser(final String valueStr) { + try { + return JavaBigDecimalParser.parseBigDecimal(valueStr); + } catch (NumberFormatException nfe) { + final String reportNum = valueStr.length() <= MAX_CHARS_TO_REPORT ? + valueStr : valueStr.substring(0, MAX_CHARS_TO_REPORT) + " [truncated]"; + throw new NumberFormatException("Value \"" + reportNum + + "\" can not be represented as `java.math.BigDecimal`, reason: " + nfe.getMessage()); + } + } + + public static BigDecimal parseWithFastParser(final char[] ch, final int off, final int len) { + try { + return JavaBigDecimalParser.parseBigDecimal(ch, off, len); + } catch (NumberFormatException nfe) { + final String reportNum = len <= MAX_CHARS_TO_REPORT ? + new String(ch, off, len) : new String(ch, off, MAX_CHARS_TO_REPORT) + " [truncated]"; + final int reportLen = Math.min(len, MAX_CHARS_TO_REPORT); + throw new NumberFormatException("Value \"" + new String(ch, off, reportLen) + + "\" can not be represented as `java.math.BigDecimal`, reason: " + nfe.getMessage()); + } + } + private static BigDecimal parseBigDecimal(final char[] chars, final int off, final int len, final int splitLen) { boolean numHasSign = false; boolean expHasSign = false; diff --git a/src/main/java/com/fasterxml/jackson/core/io/BigIntegerParser.java b/src/main/java/com/fasterxml/jackson/core/io/BigIntegerParser.java new file mode 100644 index 0000000000..c7b2e1adc7 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/core/io/BigIntegerParser.java @@ -0,0 +1,29 @@ +package com.fasterxml.jackson.core.io; + +import ch.randelshofer.fastdoubleparser.JavaBigIntegerParser; + +import java.math.BigInteger; + +import static com.fasterxml.jackson.core.io.BigDecimalParser.MAX_CHARS_TO_REPORT; + +/** + * Helper class used to implement more optimized parsing of {@link BigInteger} for REALLY + * big values (over 500 characters). + * + * @since 2.15 + */ +public final class BigIntegerParser +{ + private BigIntegerParser() {} + + public static BigInteger parseWithFastParser(final String valueStr) { + try { + return JavaBigIntegerParser.parseBigInteger(valueStr); + } catch (NumberFormatException nfe) { + final String reportNum = valueStr.length() <= MAX_CHARS_TO_REPORT ? + valueStr : valueStr.substring(0, MAX_CHARS_TO_REPORT) + " [truncated]"; + throw new NumberFormatException("Value \"" + reportNum + + "\" can not be represented as `java.math.BigDecimal`, reason: " + nfe.getMessage()); + } + } +} diff --git a/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java b/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java index 8ac3e90b44..4620c15b69 100644 --- a/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java +++ b/src/main/java/com/fasterxml/jackson/core/io/NumberInput.java @@ -380,28 +380,103 @@ public static float parseFloat(final String s, final boolean useFastParser) thro return useFastParser ? JavaFloatParser.parseFloat(s) : Float.parseFloat(s); } - public static BigDecimal parseBigDecimal(String s) throws NumberFormatException { + /** + * @param s a string representing a number to parse + * @return a BigDecimal + * @throws NumberFormatException if the char array cannot be represented by a BigDecimal + */ + public static BigDecimal parseBigDecimal(final String s) throws NumberFormatException { return BigDecimalParser.parse(s); } - public static BigDecimal parseBigDecimal(char[] ch, int off, int len) throws NumberFormatException { + /** + * @param s a string representing a number to parse + * @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser} + * @return a BigDecimal + * @throws NumberFormatException if the char array cannot be represented by a BigDecimal + * @since v2.15 + */ + public static BigDecimal parseBigDecimal(final String s, final boolean useFastParser) throws NumberFormatException { + return useFastParser ? + BigDecimalParser.parseWithFastParser(s) : + BigDecimalParser.parse(s); + } + + /** + * @param ch a char array with text that makes up a number + * @param off the offset to apply when parsing the number in the char array + * @param len the length of the number in the char array + * @return a BigDecimal + * @throws NumberFormatException if the char array cannot be represented by a BigDecimal + */ + public static BigDecimal parseBigDecimal(final char[] ch, final int off, final int len) throws NumberFormatException { return BigDecimalParser.parse(ch, off, len); } - public static BigDecimal parseBigDecimal(char[] ch) throws NumberFormatException { + /** + * @param ch a char array with text that makes up a number + * @param off the offset to apply when parsing the number in the char array + * @param len the length of the number in the char array + * @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser} + * @return a BigDecimal + * @throws NumberFormatException if the char array cannot be represented by a BigDecimal + * @since v2.15 + */ + public static BigDecimal parseBigDecimal(final char[] ch, final int off, final int len, + final boolean useFastParser) + throws NumberFormatException { + return useFastParser ? + BigDecimalParser.parseWithFastParser(ch, off, len) : + BigDecimalParser.parse(ch, off, len); + } + + /** + * @param ch a char array with text that makes up a number + * @return a BigDecimal + * @throws NumberFormatException if the char array cannot be represented by a BigDecimal + */ + public static BigDecimal parseBigDecimal(final char[] ch) throws NumberFormatException { return BigDecimalParser.parse(ch); } + /** + * @param ch a char array with text that makes up a number + * @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser} + * @return a BigDecimal + * @throws NumberFormatException if the char array cannot be represented by a BigDecimal + * @since v2.15 + */ + public static BigDecimal parseBigDecimal(final char[] ch, final boolean useFastParser) throws NumberFormatException { + return useFastParser ? + BigDecimalParser.parseWithFastParser(ch, 0, ch.length) : + BigDecimalParser.parse(ch); + } + /** * @param s a string representing a number to parse * @return a BigInteger * @throws NumberFormatException if string cannot be represented by a BigInteger * @since v2.14 */ - public static BigInteger parseBigInteger(String s) throws NumberFormatException { + public static BigInteger parseBigInteger(final String s) throws NumberFormatException { if (s.length() > LARGE_INT_SIZE) { return BigDecimalParser.parse(s).toBigInteger(); } return new BigInteger(s); } + + /** + * @param s a string representing a number to parse + * @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser} + * @return a BigInteger + * @throws NumberFormatException if string cannot be represented by a BigInteger + * @since v2.15 + */ + public static BigInteger parseBigInteger(final String s, final boolean useFastParser) throws NumberFormatException { + if (useFastParser) { + return BigIntegerParser.parseWithFastParser(s); + } else { + return parseBigInteger(s); + } + } } diff --git a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java index 6093a6306d..910d31dd3b 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java +++ b/src/main/java/com/fasterxml/jackson/core/util/TextBuffer.java @@ -484,38 +484,41 @@ public char[] contentsAsArray() { * Convenience method for converting contents of the buffer * into a {@link BigDecimal}. * + * @param constraints constraints for stream reading + * @param useFastParser whether to use {@link com.fasterxml.jackson.core.io.doubleparser} * @return Buffered text value parsed as a {@link BigDecimal}, if possible * * @throws NumberFormatException if contents are not a valid Java number * * @since 2.15 */ - public BigDecimal contentsAsDecimal(StreamReadConstraints constraints) throws NumberFormatException + public BigDecimal contentsAsDecimal(final StreamReadConstraints constraints, + final boolean useFastParser) throws NumberFormatException { // Already got a pre-cut array? if (_resultArray != null) { constraints.validateFPLength(_resultArray.length); - return NumberInput.parseBigDecimal(_resultArray); + return NumberInput.parseBigDecimal(_resultArray, useFastParser); } // Or a shared buffer? if ((_inputStart >= 0) && (_inputBuffer != null)) { constraints.validateFPLength(_inputLen); - return NumberInput.parseBigDecimal(_inputBuffer, _inputStart, _inputLen); + return NumberInput.parseBigDecimal(_inputBuffer, _inputStart, _inputLen, useFastParser); } // Or if not, just a single buffer (the usual case) if ((_segmentSize == 0) && (_currentSegment != null)) { constraints.validateFPLength(_currentSize); - return NumberInput.parseBigDecimal(_currentSegment, 0, _currentSize); + return NumberInput.parseBigDecimal(_currentSegment, 0, _currentSize, useFastParser); } // If not, let's just get it aggregated... final char[] numArray = contentsAsArray(); constraints.validateFPLength(numArray.length); - return NumberInput.parseBigDecimal(numArray); + return NumberInput.parseBigDecimal(numArray, useFastParser); } @Deprecated // @since 2.15 public BigDecimal contentsAsDecimal() throws NumberFormatException { - return contentsAsDecimal(StreamReadConstraints.defaults()); + return contentsAsDecimal(StreamReadConstraints.defaults(), false); } /** diff --git a/src/test/java/com/fasterxml/jackson/core/io/BigDecimalParserTest.java b/src/test/java/com/fasterxml/jackson/core/io/BigDecimalParserTest.java index 436d74cd1d..d02f68a4c8 100644 --- a/src/test/java/com/fasterxml/jackson/core/io/BigDecimalParserTest.java +++ b/src/test/java/com/fasterxml/jackson/core/io/BigDecimalParserTest.java @@ -2,17 +2,43 @@ public class BigDecimalParserTest extends com.fasterxml.jackson.core.BaseTest { public void testLongStringParse() { - final int len = 1500; - final StringBuilder sb = new StringBuilder(len); - for (int i = 0; i < len; i++) { - sb.append("A"); + try { + BigDecimalParser.parse(genLongString()); + fail("expected NumberFormatException"); + } catch (NumberFormatException nfe) { + assertTrue("exception message starts as expected?", nfe.getMessage().startsWith("Value \"AAAAA")); + assertTrue("exception message value contains truncated", nfe.getMessage().contains("truncated")); + } + } + + public void testLongStringFastParse() { + try { + BigDecimalParser.parseWithFastParser(genLongString()); + fail("expected NumberFormatException"); + } catch (NumberFormatException nfe) { + assertTrue("exception message starts as expected?", nfe.getMessage().startsWith("Value \"AAAAA")); + assertTrue("exception message value contains truncated", nfe.getMessage().contains("truncated")); } + } + + /* there is an open issue at https://github.com/wrandelshofer/FastDoubleParser/issues/24 about this + public void testLongStringFastParseBigInteger() { try { - BigDecimalParser.parse(sb.toString()); + BigDecimalParser.parseBigIntegerWithFastParser(genLongString()); fail("expected NumberFormatException"); } catch (NumberFormatException nfe) { assertTrue("exception message starts as expected?", nfe.getMessage().startsWith("Value \"AAAAA")); assertTrue("exception message value contains truncated", nfe.getMessage().contains("truncated")); } } + */ + + private String genLongString() { + final int len = 1500; + final StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) { + sb.append("A"); + } + return sb.toString(); + } } diff --git a/src/test/java/com/fasterxml/jackson/core/io/TestNumberInput.java b/src/test/java/com/fasterxml/jackson/core/io/TestNumberInput.java index 544aa9c85e..e1411b532e 100644 --- a/src/test/java/com/fasterxml/jackson/core/io/TestNumberInput.java +++ b/src/test/java/com/fasterxml/jackson/core/io/TestNumberInput.java @@ -35,11 +35,13 @@ public void testParseLongBigInteger() } String test1000 = stringBuilder.toString(); assertEquals(new BigInteger(test1000), NumberInput.parseBigInteger(test1000)); + assertEquals(new BigInteger(test1000), NumberInput.parseBigInteger(test1000, true)); for (int i = 0; i < 1000; i++) { stringBuilder.append(7); } String test2000 = stringBuilder.toString(); assertEquals(new BigInteger(test2000), NumberInput.parseBigInteger(test2000)); + assertEquals(new BigInteger(test2000), NumberInput.parseBigInteger(test2000, true)); } } diff --git a/src/test/java/com/fasterxml/jackson/core/read/FastParserNonStandardNumberParsingTest.java b/src/test/java/com/fasterxml/jackson/core/read/FastParserNonStandardNumberParsingTest.java index ab4a64f41f..11ef1bc18e 100644 --- a/src/test/java/com/fasterxml/jackson/core/read/FastParserNonStandardNumberParsingTest.java +++ b/src/test/java/com/fasterxml/jackson/core/read/FastParserNonStandardNumberParsingTest.java @@ -13,6 +13,7 @@ public class FastParserNonStandardNumberParsingTest .enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS) .enable(JsonReadFeature.ALLOW_TRAILING_DECIMAL_POINT_FOR_NUMBERS) .enable(StreamReadFeature.USE_FAST_DOUBLE_PARSER) + .enable(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER) .build(); @Override diff --git a/src/test/java/com/fasterxml/jackson/core/read/FastParserNumberParsingTest.java b/src/test/java/com/fasterxml/jackson/core/read/FastParserNumberParsingTest.java index fae6dda24c..ce02dd79f1 100644 --- a/src/test/java/com/fasterxml/jackson/core/read/FastParserNumberParsingTest.java +++ b/src/test/java/com/fasterxml/jackson/core/read/FastParserNumberParsingTest.java @@ -7,6 +7,7 @@ public class FastParserNumberParsingTest extends NumberParsingTest { private final JsonFactory fastFactory = JsonFactory.builder() .enable(StreamReadFeature.USE_FAST_DOUBLE_PARSER) + .enable(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER) .build(); @Override