From cd7b9e107fb6becb55bbc16ea835a13008792fe4 Mon Sep 17 00:00:00 2001 From: Khalid Qarryzada Date: Fri, 10 Jul 2026 11:37:45 -0700 Subject: [PATCH] Fix customization of JsonReadFeatures. This commit fixes an issue where setting a custom Jackson JsonReadFeature property was not reflected by the SCIM SDK's JsonMapper. Reviewer: dougbulkley Reviewer: vyhhuang JiraIssue: DS-51680 --- CHANGELOG.md | 3 ++ .../common/utils/ScimFilterJsonParser.java | 29 +++++------------- .../scim2/common/utils/ScimJsonFactory.java | 30 ++++++++++++++++++- .../scim2/common/MapperFactoryTest.java | 21 ++++++++++++- 4 files changed, 59 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ed83d63..bb383844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ was missing, ensuring that a `PatchRequest` is always available at runtime when Corrected an annotation in `PatchRequest` which did not mark `Operations` with a multiValueClass. +Fixed an issue where Jackson `JsonReadFeature` properties were not used despite the property being +set on the MapperFactory. + ## 6.0.0 - 2026-May-11 The UnboundID SCIM SDK has been updated to use version 3 of the Jackson library (this release ships with v3.1.3). This change aligns the SCIM SDK with HTTP libraries such as Spring Framework 7/Spring diff --git a/scim2-sdk-common/src/main/java/com/unboundid/scim2/common/utils/ScimFilterJsonParser.java b/scim2-sdk-common/src/main/java/com/unboundid/scim2/common/utils/ScimFilterJsonParser.java index 18242e15..a7b59467 100644 --- a/scim2-sdk-common/src/main/java/com/unboundid/scim2/common/utils/ScimFilterJsonParser.java +++ b/scim2-sdk-common/src/main/java/com/unboundid/scim2/common/utils/ScimFilterJsonParser.java @@ -37,7 +37,6 @@ import tools.jackson.core.ObjectReadContext; import tools.jackson.core.io.IOContext; import tools.jackson.core.json.JsonReadContext; -import tools.jackson.core.json.JsonReadFeature; import tools.jackson.core.json.ReaderBasedJsonParser; import tools.jackson.core.sym.CharsToNameCanonicalizer; @@ -52,16 +51,22 @@ public class ScimFilterJsonParser extends ReaderBasedJsonParser /** * Creates a Jackson-based object for parsing SCIM filters. * + * @param readContext The object read context. * @param ctxt The I/O context to use. + * @param streamFeatures The standard stream read features. + * @param formatFeatures The format stream read features. * @param r Reader used for reading actual content. * @param st The name canonicalizer to use. */ public ScimFilterJsonParser( + @NotNull final ObjectReadContext readContext, @NotNull final IOContext ctxt, + final int streamFeatures, + final int formatFeatures, @NotNull final Reader r, @NotNull final CharsToNameCanonicalizer st) { - super(getReadContext(r), ctxt, JsonReadFeature.collectDefaults(), 0, r, st); + super(readContext, ctxt, streamFeatures, formatFeatures, r, st); // By default, the JSON read context is set to StreamContext.TYPE_ROOT, // which will require whitespace after any unquoted token (e.g., a number). @@ -69,24 +74,4 @@ public ScimFilterJsonParser( // context type to -1, which is effectively "none". this._streamReadContext = new JsonReadContext(null, 0, null, -1, 1, 0); } - - /** - * Provides a Jackson reader context based on the settings established in the - * SCIM SDK's JsonMapper. - */ - @NotNull - private static ObjectReadContext getReadContext(@NotNull final Reader r) - { - // Create a parser to obtain access to an ObjectReadContext, then close the - // parser to avoid leaking an object on every invocation. When Jackson - // closes a parser, the underlying Reader, r, is not closed when the caller - // owns the resource, so this is safe. An alternative to this approach is to - // use SDK_OBJECT_MAPPER._deserializationContext() as the ObjectReadContext. - // However, this method is labelled for unit test usage only, so it is not - // guaranteed to be a stable API in future versions of Jackson. - var parser = JsonUtils.getObjectReader().createParser(r); - ObjectReadContext context = parser.objectReadContext(); - parser.close(); - return context; - } } diff --git a/scim2-sdk-common/src/main/java/com/unboundid/scim2/common/utils/ScimJsonFactory.java b/scim2-sdk-common/src/main/java/com/unboundid/scim2/common/utils/ScimJsonFactory.java index 48a4ba60..5810e869 100644 --- a/scim2-sdk-common/src/main/java/com/unboundid/scim2/common/utils/ScimJsonFactory.java +++ b/scim2-sdk-common/src/main/java/com/unboundid/scim2/common/utils/ScimJsonFactory.java @@ -36,6 +36,7 @@ import com.unboundid.scim2.common.annotations.NotNull; import tools.jackson.core.ErrorReportConfiguration; import tools.jackson.core.JsonParser; +import tools.jackson.core.ObjectReadContext; import tools.jackson.core.io.ContentReference; import tools.jackson.core.io.IOContext; import tools.jackson.core.json.JsonFactory; @@ -80,7 +81,14 @@ JsonParser createScimFilterParser(@NotNull final Reader r) ContentReference reference = ContentReference.construct(true, r, config); IOContext ctxt = _createContext(reference, false); - return new ScimFilterJsonParser(ctxt, r, _rootCharSymbols.makeChild()); + final ObjectReadContext readCtxt = getReadContext(r); + return new ScimFilterJsonParser(readCtxt, + ctxt, + readCtxt.getStreamReadFeatures(0), + readCtxt.getFormatReadFeatures(0), + r, + _rootCharSymbols.makeChild() + ); } /** @@ -94,4 +102,24 @@ public ScimJsonFactory copy() { return new ScimJsonFactory(this); } + + /** + * Provides a Jackson reader context based on the settings established in the + * SCIM SDK's JsonMapper. + */ + @NotNull + private static ObjectReadContext getReadContext(@NotNull final Reader r) + { + // Create a parser to obtain access to an ObjectReadContext, then close the + // parser to avoid leaking an object on every invocation. When Jackson + // closes a parser, the underlying Reader, r, is not closed when the caller + // owns the resource, so this is safe. An alternative to this approach is to + // use SDK_OBJECT_MAPPER._deserializationContext() as the ObjectReadContext. + // However, that method is labelled for unit test usage only, so it is not + // guaranteed to be a stable API in future versions of Jackson. + var parser = JsonUtils.getObjectReader().createParser(r); + ObjectReadContext context = parser.objectReadContext(); + parser.close(); + return context; + } } diff --git a/scim2-sdk-common/src/test/java/com/unboundid/scim2/common/MapperFactoryTest.java b/scim2-sdk-common/src/test/java/com/unboundid/scim2/common/MapperFactoryTest.java index 55ec5b15..5dd1f878 100644 --- a/scim2-sdk-common/src/test/java/com/unboundid/scim2/common/MapperFactoryTest.java +++ b/scim2-sdk-common/src/test/java/com/unboundid/scim2/common/MapperFactoryTest.java @@ -33,16 +33,20 @@ package com.unboundid.scim2.common; import com.unboundid.scim2.common.annotations.NotNull; +import com.unboundid.scim2.common.exceptions.BadRequestException; +import com.unboundid.scim2.common.filters.Filter; import com.unboundid.scim2.common.types.Email; import com.unboundid.scim2.common.types.UserResource; import com.unboundid.scim2.common.utils.JsonUtils; import com.unboundid.scim2.common.utils.MapperFactory; import org.testng.annotations.AfterMethod; import org.testng.annotations.Test; +import tools.jackson.core.json.JsonReadFeature; import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.node.ObjectNode; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static tools.jackson.databind.MapperFeature.SORT_PROPERTIES_ALPHABETICALLY; @@ -66,7 +70,7 @@ public void tearDown() * the builder provided in {@link MapperFactory#createBuilder()}. */ @Test - public void testCustomJsonMapperBuilder() + public void testCustomJsonMapperBuilder() throws Exception { // A SCIM resource with the attributes (except 'schema') sorted // alphabetically. @@ -102,6 +106,21 @@ public void testCustomJsonMapperBuilder() // sort the fields alphabetically. userJSON = JsonUtils.getObjectWriter().writeValueAsString(user); assertThat(userJSON).isEqualTo(expectedJSON); + + // Test the use of single quotes in filters, which should not be permitted + // by default. + assertThatThrownBy(() -> Filter.fromString("userName eq 'drank'")) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining("Unexpected character"); + + JsonMapper.Builder quoteCfg = JsonUtils.getInitialMapperConfig() + .enable(JsonReadFeature.ALLOW_SINGLE_QUOTES); + JsonUtils.setCustomMapperFactory(new MapperFactory().setConfig(quoteCfg)); + + // This time, the filter should be permitted. + Filter f = Filter.fromString("userName eq 'drank'"); + assertThat(f.getComparisonValue()).isNotNull(); + assertThat(f.getComparisonValue().asString()).isEqualTo("drank"); } /**