diff --git a/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java b/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java index f0e9a86984..29db980664 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java +++ b/src/main/java/com/fasterxml/jackson/core/json/JsonReadFeature.java @@ -103,7 +103,18 @@ public enum JsonReadFeature * this is a non-standard feature, and as such disabled by default. */ ALLOW_LEADING_ZEROS_FOR_NUMBERS(false), - + + /** + * Feature that determines whether parser will allow + * JSON decimal numbers to start with a deciaml point + * (like: .123). If enabled, no exception is thrown, and the number + * is parsed as though a leading 0 had been present. + *

+ * Since JSON specification does not allow leading decimal, + * this is a non-standard feature, and as such disabled by default. + */ + ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS(false), + /** * Feature that allows parser to recognize set of * "Not-a-Number" (NaN) tokens as legal floating number diff --git a/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java index 1943739ce4..a261056200 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java @@ -726,6 +726,7 @@ public final JsonToken nextToken() throws IOException case '7': case '8': case '9': + case '.': t = _parsePosNumber(i); break; default: @@ -919,6 +920,7 @@ public String nextFieldName() throws IOException case '7': case '8': case '9': + case '.': t = _parsePosNumber(i); break; case 'f': @@ -988,6 +990,7 @@ private final void _isNextTokenNameYes(int i) throws IOException case '7': case '8': case '9': + case '.': _nextToken = _parsePosNumber(i); return; } @@ -1023,6 +1026,7 @@ protected boolean _isNextTokenNameMaybe(int i, String nameToMatch) throws IOExce case '7': case '8': case '9': + case '.': t = _parsePosNumber(i); break; case 'f': @@ -1089,6 +1093,7 @@ private final JsonToken _nextTokenNotInObject(int i) throws IOException case '7': case '8': case '9': + case '.': return (_currToken = _parsePosNumber(i)); /* * This check proceeds only if the Feature.ALLOW_MISSING_VALUES is enabled @@ -1217,6 +1222,7 @@ public final Boolean nextBooleanValue() throws IOException /********************************************************** */ + /** * Initial parsing method for number values. It needs to be able * to parse enough input to be able to determine whether the @@ -1233,6 +1239,18 @@ public final Boolean nextBooleanValue() throws IOException * part of processing. */ protected final JsonToken _parsePosNumber(int ch) throws IOException + { + if (ch == '.') { + if (isEnabled(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS)) { + return (_currToken = _parsePosNumber(ch, true)); + } else { + return _handleOddValue(ch); + } + } + return _parsePosNumber(ch, false); + } + + protected final JsonToken _parsePosNumber(int ch, boolean numberHasLeadingDecimal) throws IOException { /* Although we will always be complete with respect to textual * representation (that is, all characters will be parsed), @@ -1241,6 +1259,7 @@ protected final JsonToken _parsePosNumber(int ch) throws IOException */ int ptr = _inputPtr; int startPtr = ptr-1; // to include digit already read + final int inputLen = _inputEnd; // One special case, leading zero(es): @@ -1269,7 +1288,7 @@ protected final JsonToken _parsePosNumber(int ch) throws IOException } ++intLen; } - if (ch == INT_PERIOD || ch == INT_e || ch == INT_E) { + if (ch == INT_PERIOD || ch == INT_e || ch == INT_E || numberHasLeadingDecimal) { _inputPtr = ptr; return _parseFloat(ch, startPtr, ptr, false, intLen); } diff --git a/src/main/java/com/fasterxml/jackson/core/json/UTF8DataInputJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/UTF8DataInputJsonParser.java index 8baa4464bc..ad963f4b70 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/UTF8DataInputJsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/json/UTF8DataInputJsonParser.java @@ -625,6 +625,7 @@ public JsonToken nextToken() throws IOException case '7': case '8': case '9': + case '.': t = _parsePosNumber(i); break; case 'f': @@ -690,6 +691,7 @@ private final JsonToken _nextTokenNotInObject(int i) throws IOException case '7': case '8': case '9': + case '.': return (_currToken = _parsePosNumber(i)); } return (_currToken = _handleUnexpectedValue(i)); @@ -795,6 +797,7 @@ public String nextFieldName() throws IOException case '7': case '8': case '9': + case '.': t = _parsePosNumber(i); break; case 'f': @@ -949,6 +952,16 @@ public Boolean nextBooleanValue() throws IOException */ protected JsonToken _parsePosNumber(int c) throws IOException { + + boolean forceFloat = false; + if (c == '.') { + if (isEnabled(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS)) { + forceFloat = true; + } else { + return _handleUnexpectedValue(c); + } + } + char[] outBuf = _textBuffer.emptyAndGetCurrentSegment(); int outPtr; @@ -979,7 +992,7 @@ protected JsonToken _parsePosNumber(int c) throws IOException outBuf[outPtr++] = (char) c; c = _inputData.readUnsignedByte(); } - if (c == '.' || c == 'e' || c == 'E') { + if (c == '.' || c == 'e' || c == 'E' || forceFloat) { return _parseFloat(outBuf, outPtr, c, false, intLen); } _textBuffer.setCurrentLength(outPtr); diff --git a/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java index f96896901a..d22b40059e 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java @@ -749,6 +749,7 @@ public JsonToken nextToken() throws IOException case '7': case '8': case '9': + case '.': t = _parsePosNumber(i); break; case 'f': @@ -814,6 +815,7 @@ private final JsonToken _nextTokenNotInObject(int i) throws IOException case '7': case '8': case '9': + case '.': return (_currToken = _parsePosNumber(i)); } return (_currToken = _handleUnexpectedValue(i)); @@ -930,6 +932,7 @@ public String nextFieldName() throws IOException case '7': case '8': case '9': + case '.': t = _parsePosNumber(i); break; case 'f': @@ -1142,6 +1145,7 @@ public int nextFieldName(FieldNameMatcher matcher) throws IOException case '7': case '8': case '9': + case '.': t = _parsePosNumber(i); break; case 'f': @@ -1260,6 +1264,7 @@ private final void _isNextTokenNameYes(int i) throws IOException case '7': case '8': case '9': + case '.': _nextToken = _parsePosNumber(i); return; } @@ -1317,6 +1322,7 @@ private final boolean _isNextTokenNameMaybe(int i, SerializableString str) throw case '7': case '8': case '9': + case '.': t = _parsePosNumber(i); break; default: @@ -1667,6 +1673,19 @@ public Boolean nextBooleanValue() throws IOException /********************************************************** */ + protected JsonToken _parsePosNumber(int c) throws IOException + { + if (c == '.') { + if (isEnabled(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS)) { + return (_currToken = _parsePosNumber(c, true)); + } else { + return _handleUnexpectedValue(c); + } + } + + return _parsePosNumber(c, false); + } + /** * Initial parsing method for number values. It needs to be able * to parse enough input to be able to determine whether the @@ -1682,7 +1701,7 @@ public Boolean nextBooleanValue() throws IOException * deferred, since it is usually the most complicated and costliest * part of processing. */ - protected JsonToken _parsePosNumber(int c) throws IOException + protected JsonToken _parsePosNumber(int c, boolean leadingDecimal) throws IOException { char[] outBuf = _textBuffer.emptyAndGetCurrentSegment(); // One special case: if first char is 0, must not be followed by a digit @@ -1690,9 +1709,17 @@ protected JsonToken _parsePosNumber(int c) throws IOException c = _verifyNoLeadingZeroes(); } // Ok: we can first just add digit we saw first: - outBuf[0] = (char) c; - int intLen = 1; - int outPtr = 1; + int intLen; + int outPtr; + if (leadingDecimal) { + _inputPtr--; + intLen = 0; + outPtr = 0; + } else { + intLen = 1; + outPtr = 1; + outBuf[0] = (char) c; + } // And then figure out how far we can read without further checks // for either input or output final int end = Math.min(_inputEnd, _inputPtr + outBuf.length - 1); // 1 == outPtr diff --git a/src/test/java/com/fasterxml/jackson/core/read/NumberParsingTest.java b/src/test/java/com/fasterxml/jackson/core/read/NumberParsingTest.java index abf3024c2c..43954a1af4 100644 --- a/src/test/java/com/fasterxml/jackson/core/read/NumberParsingTest.java +++ b/src/test/java/com/fasterxml/jackson/core/read/NumberParsingTest.java @@ -765,6 +765,35 @@ public void testInvalidNumber() throws Exception { } } + /** + * The format ".NNN" (as opposed to "0.NNN") is not valid JSON, so this should fail + */ + public void testLeadingDotInDecimal() throws Exception { + for (int mode : ALL_MODES) { + JsonParser p = createParser(mode, " .123 "); + try { + p.nextToken(); + fail("Should not pass"); + } catch (JsonParseException e) { + verifyException(e, "Unexpected character ('.'"); + } + p.close(); + } + } + + public void testLeadingDotInDecimalAllowed() throws Exception { + for (int mode : ALL_MODES) { + final JsonFactory f = JsonFactory.builder() + .enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS) + .build(); + JsonParser p = createParser(f, mode, " .123 "); + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + assertEquals(0.123, p.getValueAsDouble()); + assertEquals("0.123", p.getDecimalValue().toString()); + p.close(); + } + } + /* /********************************************************** /* Helper methods