diff --git a/src/main/java/com/fasterxml/jackson/core/JsonFactory.java b/src/main/java/com/fasterxml/jackson/core/JsonFactory.java index 3fa4240070..c02ab9254d 100644 --- a/src/main/java/com/fasterxml/jackson/core/JsonFactory.java +++ b/src/main/java/com/fasterxml/jackson/core/JsonFactory.java @@ -284,6 +284,14 @@ public static int collectDefaults() { */ protected StreamReadConstraints _streamReadConstraints; + /** + * Container for configuration values used when handling errorneous token inputs. + * + * @see ErrorReportConfiguration + * @since 2.16 + */ + protected ErrorReportConfiguration _errorReportConfiguration; + /** * Write constraints to use for {@link JsonGenerator}s constructed using * this factory. @@ -361,6 +369,7 @@ public JsonFactory(ObjectCodec oc) { _quoteChar = DEFAULT_QUOTE_CHAR; _streamReadConstraints = StreamReadConstraints.defaults(); _streamWriteConstraints = StreamWriteConstraints.defaults(); + _errorReportConfiguration = ErrorReportConfiguration.defaults(); _generatorDecorators = null; } @@ -385,6 +394,7 @@ protected JsonFactory(JsonFactory src, ObjectCodec codec) _generatorDecorators = _copy(src._generatorDecorators); _streamReadConstraints = Objects.requireNonNull(src._streamReadConstraints); _streamWriteConstraints = Objects.requireNonNull(src._streamWriteConstraints); + _errorReportConfiguration = Objects.requireNonNull(src._errorReportConfiguration); // JSON-specific _characterEscapes = src._characterEscapes; @@ -412,6 +422,7 @@ public JsonFactory(JsonFactoryBuilder b) { _generatorDecorators = _copy(b._generatorDecorators); _streamReadConstraints = Objects.requireNonNull(b._streamReadConstraints); _streamWriteConstraints = Objects.requireNonNull(b._streamWriteConstraints); + _errorReportConfiguration = Objects.requireNonNull(b._errorReportConfiguration); // JSON-specific _characterEscapes = b._characterEscapes; @@ -439,6 +450,7 @@ protected JsonFactory(TSFBuilder b, boolean bogus) { _generatorDecorators = _copy(b._generatorDecorators); _streamReadConstraints = Objects.requireNonNull(b._streamReadConstraints); _streamWriteConstraints = Objects.requireNonNull(b._streamWriteConstraints); + _errorReportConfiguration = Objects.requireNonNull(b._errorReportConfiguration); // JSON-specific: need to assign even if not really used _characterEscapes = null; @@ -838,6 +850,26 @@ public JsonFactory setStreamReadConstraints(StreamReadConstraints src) { return this; } + /** + * Method for overriding {@link ErrorReportConfiguration} defined for + * this factory. + *

+ * NOTE: the preferred way to set constraints is by using + * {@link JsonFactoryBuilder#errorReportConfiguration}: this method is only + * provided to support older non-builder-based construction. + * In Jackson 3.x this method will not be available. + * + * @param src Configuration + * + * @return This factory instance (to allow call chaining) + * + * @since 2.16 + */ + public JsonFactory setErrorReportConfiguration(ErrorReportConfiguration src) { + _errorReportConfiguration = Objects.requireNonNull(src, "Cannot pass null ErrorReportConfiguration");; + return this; + } + /** * Method for overriding {@link StreamWriteConstraints} defined for * this factory. @@ -2112,7 +2144,7 @@ protected IOContext _createContext(ContentReference contentRef, boolean resource if (contentRef == null) { contentRef = ContentReference.unknown(); } - return new IOContext(_streamReadConstraints, _streamWriteConstraints, + return new IOContext(_streamReadConstraints, _streamWriteConstraints, _errorReportConfiguration, _getBufferRecycler(), contentRef, resourceManaged); } @@ -2128,7 +2160,7 @@ protected IOContext _createContext(ContentReference contentRef, boolean resource */ @Deprecated // @since 2.13 protected IOContext _createContext(Object rawContentRef, boolean resourceManaged) { - return new IOContext(_streamReadConstraints, _streamWriteConstraints, + return new IOContext(_streamReadConstraints, _streamWriteConstraints, _errorReportConfiguration, _getBufferRecycler(), _createContentReference(rawContentRef), resourceManaged); @@ -2147,7 +2179,7 @@ protected IOContext _createContext(Object rawContentRef, boolean resourceManaged protected IOContext _createNonBlockingContext(Object srcRef) { // [jackson-core#479]: allow recycling for non-blocking parser again // now that access is thread-safe - return new IOContext(_streamReadConstraints, _streamWriteConstraints, + return new IOContext(_streamReadConstraints, _streamWriteConstraints, _errorReportConfiguration, _getBufferRecycler(), _createContentReference(srcRef), false); @@ -2168,7 +2200,7 @@ protected IOContext _createNonBlockingContext(Object srcRef) { protected ContentReference _createContentReference(Object contentAccessor) { // 21-Mar-2021, tatu: For now assume "canHandleBinaryNatively()" is reliable // indicator of textual vs binary format: - return ContentReference.construct(!canHandleBinaryNatively(), contentAccessor); + return ContentReference.construct(!canHandleBinaryNatively(), contentAccessor, _errorReportConfiguration); } /** @@ -2192,7 +2224,7 @@ protected ContentReference _createContentReference(Object contentAccessor, // 21-Mar-2021, tatu: For now assume "canHandleBinaryNatively()" is reliable // indicator of textual vs binary format: return ContentReference.construct(!canHandleBinaryNatively(), - contentAccessor, offset, length); + contentAccessor, offset, length, _errorReportConfiguration); } /* diff --git a/src/main/java/com/fasterxml/jackson/core/JsonProcessingException.java b/src/main/java/com/fasterxml/jackson/core/JsonProcessingException.java index e3364e9039..11d1cd88db 100644 --- a/src/main/java/com/fasterxml/jackson/core/JsonProcessingException.java +++ b/src/main/java/com/fasterxml/jackson/core/JsonProcessingException.java @@ -14,6 +14,8 @@ *

* Since Jackson 2.12 extends intermediate {@link JacksonException} type * instead of directly extending {@link java.io.IOException}. + *

+ * Since Jackson 2.16, handles its content as configured using {@link com.fasterxml.jackson.core.ErrorReportConfiguration}. */ public class JsonProcessingException extends JacksonException { diff --git a/src/main/java/com/fasterxml/jackson/core/TSFBuilder.java b/src/main/java/com/fasterxml/jackson/core/TSFBuilder.java index 02fe83b0eb..e3984daf8c 100644 --- a/src/main/java/com/fasterxml/jackson/core/TSFBuilder.java +++ b/src/main/java/com/fasterxml/jackson/core/TSFBuilder.java @@ -100,6 +100,13 @@ public abstract class TSFBuilder _generatorDecorators; + /** + * Optional {@link ErrorReportConfiguration} to use. + * + * @since 2.16 + */ + protected ErrorReportConfiguration _errorReportConfiguration; + /* /********************************************************************** /* Construction @@ -120,6 +127,7 @@ protected TSFBuilder(JsonFactory base) _outputDecorator = base._outputDecorator; _streamReadConstraints = base._streamReadConstraints; _streamWriteConstraints = base._streamWriteConstraints; + _errorReportConfiguration = base._errorReportConfiguration; _generatorDecorators = _copy(base._generatorDecorators); } @@ -134,6 +142,7 @@ protected TSFBuilder(int factoryFeatures, _outputDecorator = null; _streamReadConstraints = StreamReadConstraints.defaults(); _streamWriteConstraints = StreamWriteConstraints.defaults(); + _errorReportConfiguration = ErrorReportConfiguration.defaults(); _generatorDecorators = null; } @@ -324,6 +333,17 @@ public B streamReadConstraints(StreamReadConstraints streamReadConstraints) { return _this(); } + /** + * Sets the configuration for error tokens. + * + * @param errorReportConfiguration configuration values used for handling errorneous token inputs. + * @return this factory + * @since 2.16 + */ + public B errorReportConfiguration(ErrorReportConfiguration errorReportConfiguration) { + _errorReportConfiguration = errorReportConfiguration; + return _this(); + } /** * Sets the constraints for streaming writes. * diff --git a/src/test/java/com/fasterxml/jackson/core/ErrorReportConfigurationMaxRawContentLengthTest.java b/src/test/java/com/fasterxml/jackson/core/ErrorReportConfigurationMaxRawContentLengthTest.java deleted file mode 100644 index 3afd9fd011..0000000000 --- a/src/test/java/com/fasterxml/jackson/core/ErrorReportConfigurationMaxRawContentLengthTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.fasterxml.jackson.core; - -import com.fasterxml.jackson.core.io.ContentReference; - -/** - * Unit tests for class {@link ErrorReportConfiguration#getMaxRawContentLength()}. - */ -public class ErrorReportConfigurationMaxRawContentLengthTest extends BaseTest { - /* - /********************************************************** - /* Unit Tests - /********************************************************** - */ - - public void testBasicToStringErrorConfig() throws Exception { - // Truncated result - _verifyToString("abc", 2, - "[Source: (String)\"ab\"[truncated 1 chars]; line: 1, column: 1]"); - // Exact length - _verifyToString("abc", 3, - "[Source: (String)\"abc\"; line: 1, column: 1]"); - // Enough length - _verifyToString("abc", 4, - "[Source: (String)\"abc\"; line: 1, column: 1]"); - } - - /* - /********************************************************** - /* Internal helper methods - /********************************************************** - */ - - private void _verifyToString(String rawSrc, int rawContentLength, String expectedMessage) { - ContentReference reference = _sourceRefWithErrorReportConfig(rawSrc, rawContentLength); - String location = new JsonLocation(reference, 10L, 10L, 1, 1).toString(); - assertEquals(expectedMessage, location); - } - - private ContentReference _sourceRefWithErrorReportConfig(String rawSrc, int rawContentLength) { - return _sourceRef(rawSrc, - ErrorReportConfiguration.builder().maxRawContentLength(rawContentLength).build()); - } - - private ContentReference _sourceRef(String rawSrc, ErrorReportConfiguration errorReportConfiguration) { - return ContentReference.construct(true, rawSrc, 0, rawSrc.length(),errorReportConfiguration); - } - -} diff --git a/src/test/java/com/fasterxml/jackson/core/ErrorReportConfigurationTest.java b/src/test/java/com/fasterxml/jackson/core/ErrorReportConfigurationTest.java index 4c48f02794..956d07d070 100644 --- a/src/test/java/com/fasterxml/jackson/core/ErrorReportConfigurationTest.java +++ b/src/test/java/com/fasterxml/jackson/core/ErrorReportConfigurationTest.java @@ -1,5 +1,8 @@ package com.fasterxml.jackson.core; +import static org.assertj.core.api.Assertions.assertThat; +import com.fasterxml.jackson.core.io.ContentReference; + /** * Unit tests for class {@link ErrorReportConfiguration}. * @@ -8,6 +11,17 @@ public class ErrorReportConfigurationTest extends BaseTest { + + /* + /********************************************************** + /* Unit Tests + /********************************************************** + */ + + private final int DEFAULT_CONTENT_LENGTH = ErrorReportConfiguration.DEFAULT_MAX_RAW_CONTENT_LENGTH; + + private final int DEFAULT_ERROR_LENGTH = ErrorReportConfiguration.DEFAULT_MAX_ERROR_TOKEN_LENGTH; + private final ErrorReportConfiguration DEFAULTS = ErrorReportConfiguration.defaults(); public void testNormalBuild() @@ -55,8 +69,8 @@ public void testInvalidMaxErrorTokenLength() public void testDefaults() { // default value - assertEquals(ErrorReportConfiguration.DEFAULT_MAX_ERROR_TOKEN_LENGTH, DEFAULTS.getMaxErrorTokenLength()); - assertEquals(ErrorReportConfiguration.DEFAULT_MAX_RAW_CONTENT_LENGTH, DEFAULTS.getMaxRawContentLength()); + assertEquals(DEFAULT_ERROR_LENGTH, DEFAULTS.getMaxErrorTokenLength()); + assertEquals(DEFAULT_CONTENT_LENGTH, DEFAULTS.getMaxRawContentLength()); // equals assertEquals(ErrorReportConfiguration.defaults(), ErrorReportConfiguration.defaults()); @@ -69,8 +83,8 @@ public void testOverrideDefaultErrorReportConfiguration() try { ErrorReportConfiguration nullDefaults = ErrorReportConfiguration.defaults(); - assertEquals(ErrorReportConfiguration.DEFAULT_MAX_ERROR_TOKEN_LENGTH, nullDefaults.getMaxErrorTokenLength()); - assertEquals(ErrorReportConfiguration.DEFAULT_MAX_RAW_CONTENT_LENGTH, nullDefaults.getMaxRawContentLength()); + assertEquals(DEFAULT_ERROR_LENGTH, nullDefaults.getMaxErrorTokenLength()); + assertEquals(DEFAULT_CONTENT_LENGTH, nullDefaults.getMaxRawContentLength()); // (2) override with other value that actually changes default values ErrorReportConfiguration.overrideDefaultErrorReportConfiguration(ErrorReportConfiguration.builder() @@ -86,8 +100,8 @@ public void testOverrideDefaultErrorReportConfiguration() // (3) revert back to default values // IMPORTANT : make sure to revert back, otherwise other tests will be affected ErrorReportConfiguration.overrideDefaultErrorReportConfiguration(ErrorReportConfiguration.builder() - .maxErrorTokenLength(ErrorReportConfiguration.DEFAULT_MAX_ERROR_TOKEN_LENGTH) - .maxRawContentLength(ErrorReportConfiguration.DEFAULT_MAX_RAW_CONTENT_LENGTH) + .maxErrorTokenLength(DEFAULT_ERROR_LENGTH) + .maxRawContentLength(DEFAULT_CONTENT_LENGTH) .build()); } } @@ -113,4 +127,187 @@ public void testBuilderConstructorWithErrorReportConfiguration() assertEquals(configA.getMaxErrorTokenLength(), configB.getMaxErrorTokenLength()); assertEquals(configA.getMaxRawContentLength(), configB.getMaxRawContentLength()); } + + public void testWithJsonLocation() throws Exception + { + // Truncated result + _verifyJsonLocationToString("abc", 2, "\"ab\"[truncated 1 chars]"); + // Exact length + _verifyJsonLocationToString("abc", 3, "\"abc\""); + // Enough length + _verifyJsonLocationToString("abc", 4, "\"abc\""); + } + + public void testWithJsonFactory() throws Exception + { + // default + _verifyJsonProcessingExceptionSourceLength(500, + ErrorReportConfiguration.builder().build()); + // default + _verifyJsonProcessingExceptionSourceLength(500, + ErrorReportConfiguration.defaults()); + // shorter + _verifyJsonProcessingExceptionSourceLength(499, + ErrorReportConfiguration.builder() + .maxRawContentLength(DEFAULT_CONTENT_LENGTH - 1).build()); + // longer + _verifyJsonProcessingExceptionSourceLength(501, + ErrorReportConfiguration.builder() + .maxRawContentLength(DEFAULT_CONTENT_LENGTH + 1).build()); + // zero + _verifyJsonProcessingExceptionSourceLength(0, + ErrorReportConfiguration.builder() + .maxRawContentLength(0).build()); + } + + public void testExpectedTokenLengthWithConfigurations() + throws Exception + { + // default + _verifyErrorTokenLength(263, + ErrorReportConfiguration.builder().build()); + // default + _verifyErrorTokenLength(263, + ErrorReportConfiguration.defaults()); + // shorter + _verifyErrorTokenLength(63, + ErrorReportConfiguration.builder() + .maxErrorTokenLength(DEFAULT_ERROR_LENGTH - 200).build()); + // longer + _verifyErrorTokenLength(463, + ErrorReportConfiguration.builder() + .maxErrorTokenLength(DEFAULT_ERROR_LENGTH + 200).build()); + // zero + _verifyErrorTokenLength(9, + ErrorReportConfiguration.builder() + .maxErrorTokenLength(0).build()); + + // negative value fails + try { + _verifyErrorTokenLength(9, + ErrorReportConfiguration.builder() + .maxErrorTokenLength(-1).build()); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()) + .contains("Value of maxErrorTokenLength") + .contains("cannot be negative"); + } + // null is not allowed, throws NPE + try { + _verifyErrorTokenLength(263, + null); + } catch (NullPointerException e) { + // no-op + } + } + + public void testNonPositiveErrorTokenConfig() + { + // Zero should be ok + ErrorReportConfiguration.builder().maxErrorTokenLength(0).build(); + + // But not -1 + try { + ErrorReportConfiguration.builder().maxErrorTokenLength(-1).build(); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()) + .contains("Value of maxErrorTokenLength") + .contains("cannot be negative"); + } + } + + public void testNullSetterThrowsException() { + try { + newStreamFactory().setErrorReportConfiguration(null); + fail(); + } catch (NullPointerException npe) { + assertThat(npe).hasMessage("Cannot pass null ErrorReportConfiguration"); + } + } + + /* + /********************************************************** + /* Internal helper methods + /********************************************************** + */ + + private void _verifyJsonProcessingExceptionSourceLength(int expectedRawContentLength, ErrorReportConfiguration erc) + throws Exception + { + // Arrange + JsonFactory factory = streamFactoryBuilder() + .enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION) + .errorReportConfiguration(erc) + .build(); + // Make JSON input too long so it can be cutoff + int tooLongContent = 50 * DEFAULT_CONTENT_LENGTH; + String inputWithDynamicLength = _buildBrokenJsonOfLength(tooLongContent); + + // Act + try (JsonParser parser = factory.createParser(inputWithDynamicLength)) { + parser.nextToken(); + parser.nextToken(); + fail("Should not reach"); + } catch (JsonProcessingException e) { + + // Assert + String prefix = "(String)\""; + String suffix = "\"[truncated 12309 chars]"; + + // The length of the source description should be [ prefix + expected length + suffix ] + int expectedLength = prefix.length() + expectedRawContentLength + suffix.length(); + int actualLength = e.getLocation().sourceDescription().length(); + + assertEquals(expectedLength, actualLength); + assertThat(e.getMessage()) + .contains("Unrecognized token '") + .contains("was expecting (JSON"); + } + } + + private void _verifyJsonLocationToString(String rawSrc, int rawContentLength, String expectedMessage) + { + ErrorReportConfiguration erc = ErrorReportConfiguration.builder() + .maxRawContentLength(rawContentLength) + .build(); + ContentReference reference = ContentReference.construct(true, rawSrc, 0, rawSrc.length(), erc); + assertEquals( + "[Source: (String)" + expectedMessage + "; line: 1, column: 1]", + new JsonLocation(reference, 10L, 10L, 1, 1).toString()); + } + + private void _verifyErrorTokenLength(int expectedTokenLen, ErrorReportConfiguration errorReportConfiguration) + throws Exception + { + JsonFactory jf3 = streamFactoryBuilder() + .errorReportConfiguration(errorReportConfiguration) + .build(); + _testWithMaxErrorTokenLength(expectedTokenLen, + // creating arbitrary number so that token reaches max len, but not over-do it + 50 * DEFAULT_ERROR_LENGTH, jf3); + } + + private void _testWithMaxErrorTokenLength(int expectedSize, int tokenLen, JsonFactory factory) + throws Exception + { + String inputWithDynamicLength = _buildBrokenJsonOfLength(tokenLen); + try (JsonParser parser = factory.createParser(inputWithDynamicLength)) { + parser.nextToken(); + parser.nextToken(); + } catch (JsonProcessingException e) { + assertThat(e.getLocation()._totalChars).isEqualTo(expectedSize); + assertThat(e.getMessage()).contains("Unrecognized token"); + } + } + + private String _buildBrokenJsonOfLength(int len) + { + StringBuilder sb = new StringBuilder("{\"key\":"); + for (int i = 0; i < len; i++) { + sb.append("a"); + } + sb.append("!}"); + return sb.toString(); + } }