Skip to content

Commit

Permalink
Support customization of the MapperFactory
Browse files Browse the repository at this point in the history
Previously, the fields of the MapperFactory were static. This
effectively allowed only a single instance of the object, and also made
it impossible to override the createObjectMapper() method with personal
customizations like adding serializers/deserializers. The SCIM SDK
already treated MapperFactory instances as if they were not static, so
we will now permit flexible customization. The documentation makes it
clear that the superclass's object mapper must be fetched first for
optimal behavior.

Reviewer: vyhhuang
Reviewer: dougbulkley

JiraIssue: DS-49067
  • Loading branch information
kqarryzada committed Jul 22, 2024
1 parent 50aec55 commit c5a828d
Show file tree
Hide file tree
Showing 3 changed files with 360 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.unboundid.scim2.common.annotations.NotNull;

Expand All @@ -32,29 +33,97 @@
import java.util.Date;
import java.util.Map;

import static com.fasterxml.jackson.databind.MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES;

/**
* Class used to customize the object mapper that is used by the SCIM 2 SDK.
* This class may be used to customize the object mapper that is used by the
* SCIM SDK.
* <br><br>
*
* The SCIM SDK uses a Jackson {@link ObjectMapper} to convert SCIM resources
* between JSON strings and Plain Old Java Objects such as
* {@link com.unboundid.scim2.common.types.UserResource}. This object mapper is
* configured with specific settings to benefit applications that use the SCIM
* SDK. For example, when converting a Java object to a JSON string, the SCIM
* SDK will ignore {@code null} fields from the object.
* <br><br>
*
* If your project would benefit from enabling or disabling certain Jackson
* features on the SCIM SDK's object mapper, use one of the following methods:
* <ul>
* <li> {@link #setMapperCustomFeatures}
* <li> {@link #setDeserializationCustomFeatures}
* <li> {@link #setSerializationCustomFeatures}
* <li> {@link #setJsonParserCustomFeatures}
* <li> {@link #setJsonGeneratorCustomFeatures}
* </ul>
*
* For example, to disable the
* {@link MapperFeature#ACCEPT_CASE_INSENSITIVE_PROPERTIES} property, use the
* following Java code:
* <pre>
* MapperFactory newFactory = new MapperFactory();
* newFactory.setMapperCustomFeatures(
* Map.of(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, false)
* );
*
* // Register the new MapperFactory with the SCIM SDK.
* JsonUtils.setCustomMapperFactory(newFactory);
* </pre>
*
* If your desired customization is more complicated than enabling/disabling
* Jackson features, an alternative is to create a custom {@code MapperFactory}
* class that overrides the behavior of {@link #createObjectMapper}. When
* overriding this method, the subclass <em>must</em> first fetch an object
* mapper from the superclass to ensure that the SCIM SDK's original
* configuration is preserved. For example:
* <pre>
* public class CustomMapperFactory extends MapperFactory
* {
* {@literal @}Override
* public ObjectMapper createObjectMapper()
* {
* // Fetch the initial object mapper from the superclass, then add your
* // customizations. Do not instantiate a new ObjectMapper.
* ObjectMapper mapper = super.createObjectMapper();
*
* // Add the desired customizations.
* SimpleModule module = new SimpleModule();
* module.addSerializer(DesiredClass.class, new CustomSerializer());
* module.addDeserializer(DesiredClass.class, new CustomDeserializer());
* mapper.registerModule(module);
*
* return mapper;
* }
* }
* </pre>
*
* When your application starts up, register your customer mapper factory with
* the SCIM SDK to use the object mapper returned by the custom class:
* <pre>
* JsonUtils.setCustomMapperFactory(new CustomMapperFactory());
* </pre>
*/
public class MapperFactory
{
@NotNull
private static Map<DeserializationFeature, Boolean> deserializationCustomFeatures =
private Map<DeserializationFeature, Boolean> deserializationCustomFeatures =
Collections.emptyMap();

@NotNull
private static Map<JsonParser.Feature, Boolean> jsonParserCustomFeatures =
private Map<JsonParser.Feature, Boolean> jsonParserCustomFeatures =
Collections.emptyMap();

@NotNull
private static Map<JsonGenerator.Feature, Boolean> jsonGeneratorCustomFeatures =
private Map<JsonGenerator.Feature, Boolean> jsonGeneratorCustomFeatures =
Collections.emptyMap();

@NotNull
private static Map<MapperFeature, Boolean> mapperCustomFeatures =
private Map<MapperFeature, Boolean> mapperCustomFeatures =
Collections.emptyMap();

@NotNull
private static Map<SerializationFeature, Boolean> serializationCustomFeatures =
private Map<SerializationFeature, Boolean> serializationCustomFeatures =
Collections.emptyMap();

/**
Expand Down Expand Up @@ -152,9 +221,20 @@ public MapperFactory setSerializationCustomFeatures(
* and deserializing SCIM JSON objects.
*/
@NotNull
public static ObjectMapper createObjectMapper()
public ObjectMapper createObjectMapper()
{
ObjectMapper mapper = new ObjectMapper(new ScimJsonFactory());
// Create a new object mapper with case-insensitive settings.
var objectMapperBuilder = JsonMapper.builder(new ScimJsonFactory());

// Do not care about case when de-serializing POJOs.
objectMapperBuilder.enable(ACCEPT_CASE_INSENSITIVE_PROPERTIES);

// Add any custom mapper features. This must be done before other fields
// (e.g., serializationCustomFeatures) because it must be configured on the
// builder object.
mapperCustomFeatures.forEach(objectMapperBuilder::configure);

final ObjectMapper mapper = objectMapperBuilder.build();

// Don't serialize POJO nulls as JSON nulls.
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
Expand All @@ -168,36 +248,16 @@ public static ObjectMapper createObjectMapper()
dateTimeModule.addDeserializer(Date.class, new DateDeserializer());
mapper.registerModule(dateTimeModule);

// Do not care about case when de-serializing POJOs.
mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);

// Use the case-insensitive JsonNodes.
mapper.setNodeFactory(new ScimJsonNodeFactory());

for (DeserializationFeature feature : deserializationCustomFeatures.keySet())
{
mapper.configure(feature, deserializationCustomFeatures.get(feature));
}

for (JsonGenerator.Feature feature : jsonGeneratorCustomFeatures.keySet())
{
mapper.configure(feature, jsonGeneratorCustomFeatures.get(feature));
}

for (JsonParser.Feature feature : jsonParserCustomFeatures.keySet())
{
mapper.configure(feature, jsonParserCustomFeatures.get(feature));
}

for (MapperFeature feature : mapperCustomFeatures.keySet())
{
mapper.configure(feature, mapperCustomFeatures.get(feature));
}

for (SerializationFeature feature : serializationCustomFeatures.keySet())
{
mapper.configure(feature, serializationCustomFeatures.get(feature));
}
// Configure the custom Jackson features for object mappers created and used
// by the SCIM SDK. This step is performed last to ensure that
// customizations are not overwritten.
deserializationCustomFeatures.forEach(mapper::configure);
jsonGeneratorCustomFeatures.forEach(mapper::configure);
jsonParserCustomFeatures.forEach(mapper::configure);
serializationCustomFeatures.forEach(mapper::configure);

return mapper;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,14 @@

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableMap;
import com.unboundid.scim2.common.exceptions.ScimException;
import com.unboundid.scim2.common.filters.Filter;
import com.unboundid.scim2.common.types.Name;
import com.unboundid.scim2.common.utils.DateTimeUtils;
import com.unboundid.scim2.common.utils.JsonUtils;
import com.unboundid.scim2.common.utils.MapperFactory;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
Expand Down Expand Up @@ -1180,38 +1176,6 @@ public Object[][] getPathExistsParams() throws Exception
};
}

/**
* Test that setting a custom object mapper factory allows for custom
* options such as setting fail on unknown properties deserialization option
* to false (defaults to true).
*
* @throws Exception if an error occurs.
*/
@Test
public void testCustomMapper() throws Exception
{
MapperFactory mapperFactory = new MapperFactory();
mapperFactory.setDeserializationCustomFeatures(
ImmutableMap.<DeserializationFeature, Boolean>builder().
put(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, Boolean.FALSE).build());
JsonUtils.setCustomMapperFactory(mapperFactory);

String jsonNameString =
"{" +
"\"familyName\":\"Smith\"," +
"\"givenName\":\"Bob\"," +
"\"middleName\":\"X\"," +
"\"bogusField\":\"bogusValue\"" +
"}";

Name name = JsonUtils.getObjectReader().
forType(Name.class).readValue(jsonNameString);

Assert.assertEquals(name.getFamilyName(), "Smith");
Assert.assertEquals(name.getGivenName(), "Bob");
Assert.assertEquals(name.getMiddleName(), "X");
}

/**
* Test that the SCIM 2 SDK ObjectMapper ignores null map values.
*/
Expand Down
Loading

0 comments on commit c5a828d

Please sign in to comment.