Java tutorial
/* * Copyright 2015-2017 UnboundID Corp. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License (GPLv2 only) * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see <http://www.gnu.org/licenses>. */ package com.unboundid.scim2.common.utils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.type.CollectionType; import com.fasterxml.jackson.databind.util.ISO8601Utils; import com.unboundid.scim2.common.Path; import com.unboundid.scim2.common.exceptions.BadRequestException; import com.unboundid.scim2.common.exceptions.ScimException; import com.unboundid.scim2.common.filters.Filter; import com.unboundid.scim2.common.messages.PatchOperation; import com.unboundid.scim2.common.types.AttributeDefinition; import java.io.IOException; import java.text.ParseException; import java.text.ParsePosition; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; /** * Utility methods to manipulate JSON nodes using paths. */ public class JsonUtils { private static MapperFactory mapperFactory = new MapperFactory(); private static ObjectMapper SDK_OBJECT_MAPPER = createObjectMapper(); public abstract static class NodeVisitor { /** * Visit a node referenced by an path element before that last element. * * @param parent The parent container ObjectNode. * @param field The field to visit. * @param valueFilter the filter for the value(s) to visit. * @return The JsonNode referenced by the element in the parent. * @throws ScimException If an error occurs. */ abstract JsonNode visitInnerNode(final ObjectNode parent, final String field, final Filter valueFilter) throws ScimException; /** * Visit a node referenced by the last path element. * * @param parent The parent container ObjectNode. * @param field The field to visit. * @param valueFilter the filter for the value(s) to visit. * @throws ScimException If an error occurs. */ abstract void visitLeafNode(final ObjectNode parent, final String field, final Filter valueFilter) throws ScimException; /** * * @param array The ArrayNode to filter. * @param valueFilter The value filter. * @param removeMatching {@code true} to remove matching values or * {@code false} otherwise. * @return The matching values. * @throws ScimException If an error occurs. */ ArrayNode filterArray(final ArrayNode array, final Filter valueFilter, final boolean removeMatching) throws ScimException { ArrayNode matchingArray = getJsonNodeFactory().arrayNode(); Iterator<JsonNode> i = array.elements(); while (i.hasNext()) { JsonNode node = i.next(); if (FilterEvaluator.evaluate(valueFilter, node)) { matchingArray.add(node); if (removeMatching) { i.remove(); } } } return matchingArray; } } private static final class GatheringNodeVisitor extends NodeVisitor { final List<JsonNode> values = new LinkedList<JsonNode>(); final boolean removeValues; /** * Create a new GatheringNodeVisitor. * * @param removeValues {@code true} to remove the gathered values from * the container node or {@code false} otherwise. */ private GatheringNodeVisitor(final boolean removeValues) { this.removeValues = removeValues; } /** * {@inheritDoc} */ JsonNode visitInnerNode(final ObjectNode parent, final String field, final Filter valueFilter) throws ScimException { JsonNode node = parent.path(field); if (node.isArray() && valueFilter != null) { return filterArray((ArrayNode) node, valueFilter, false); } return node; } /** * {@inheritDoc} */ void visitLeafNode(final ObjectNode parent, final String field, final Filter valueFilter) throws ScimException { JsonNode node = parent.path(field); if (node.isArray()) { ArrayNode arrayNode = (ArrayNode) node; if (valueFilter != null) { arrayNode = filterArray((ArrayNode) node, valueFilter, removeValues); } if (arrayNode.size() > 0) { values.add(arrayNode); } if (removeValues && (valueFilter == null || node.size() == 0)) { // There are no more values left after removing the matching values. // Just remove the field. parent.remove(field); } } else if (node.isObject() || node.isValueNode()) { values.add(node); if (removeValues) { parent.remove(field); } } } } public static class UpdatingNodeVisitor extends NodeVisitor { /** * The updated value. */ protected final JsonNode value; /** * Whether to append or replace array values. */ protected final boolean appendValues; /** * Create a new UpdatingNodeVisitor. * * @param value The update value. * @param appendValues {@code true} to append the update value or * {@code false} otherwise. */ protected UpdatingNodeVisitor(final JsonNode value, final boolean appendValues) { this.value = value.deepCopy(); this.appendValues = appendValues; } /** * {@inheritDoc} */ protected JsonNode visitInnerNode(final ObjectNode parent, final String field, final Filter valueFilter) throws ScimException { JsonNode node = parent.path(field); if (node.isValueNode() || ((node.isMissingNode() || node.isNull()) && valueFilter != null)) { throw BadRequestException .noTarget("Attribute " + field + " does not have a multi-valued or " + "complex value"); } if (node.isMissingNode() || node.isNull()) { // Create the missing node as an JSON object node. ObjectNode newObjectNode = getJsonNodeFactory().objectNode(); parent.set(field, newObjectNode); return newObjectNode; } else if (node.isArray()) { ArrayNode arrayNode = (ArrayNode) node; if (valueFilter != null) { arrayNode = filterArray((ArrayNode) node, valueFilter, false); if (arrayNode.size() == 0) { throw BadRequestException.noTarget("Attribute " + field + " does not have a value matching the " + "filter " + valueFilter); } } return arrayNode; } return node; } /** * {@inheritDoc} */ protected void visitLeafNode(final ObjectNode parent, final String field, final Filter valueFilter) throws ScimException { if (field != null) { JsonNode node = parent.path(field); if (!appendValues && valueFilter != null) { // in replace mode, a value filter requires that the target node // be an array and that we can find matching value(s) boolean matchesFound = false; if (node.isArray()) { for (int i = 0; i < node.size(); i++) { if (FilterEvaluator.evaluate(valueFilter, node.get(i))) { matchesFound = true; if (node.get(i).isObject() && value.isObject()) { updateNode((ObjectNode) node.get(i), null, value); } else { ((ArrayNode) node).set(i, value); } } } } // exception: this allows filters on singular values if // and only if the filter uses the "value" attribute to // reference the value of the value node. else if (FilterEvaluator.evaluate(valueFilter, node)) { matchesFound = true; updateNode(parent, field, value); } if (!matchesFound) { throw BadRequestException.noTarget("Attribute " + field + " does not have a value matching " + "the filter " + valueFilter.toString()); } return; } } updateNode(parent, field, value); } /** * Update the value(s) of the field specified by the key in the parent * container node. * * @param parent The container node. * @param key The key of the field to update. * @param value The update value. */ protected void updateNode(final ObjectNode parent, final String key, final JsonNode value) { if (value.isNull() || value.isArray() && value.size() == 0) { // draft-ietf-scim-core-schema section 2.4 states "Unassigned // attributes, the null value, or empty array (in the case of // a multi-valued attribute) SHALL be considered to be // equivalent in "state". return; } // When key is null, the node to update is the parent it self. JsonNode node = key == null ? parent : parent.path(key); if (node.isObject()) { if (value.isObject()) { // Go through the fields of both objects and merge them. ObjectNode targetObject = (ObjectNode) node; ObjectNode valueObject = (ObjectNode) value; Iterator<Map.Entry<String, JsonNode>> i = valueObject.fields(); while (i.hasNext()) { Map.Entry<String, JsonNode> field = i.next(); updateNode(targetObject, field.getKey(), field.getValue()); } } else { // Replace the field. parent.set(key, value); } } else if (node.isArray()) { if (value.isArray() && appendValues) { // Append the new values to the existing ones. ArrayNode targetArray = (ArrayNode) node; ArrayNode valueArray = (ArrayNode) value; for (JsonNode valueNode : valueArray) { boolean valueFound = false; for (JsonNode targetNode : targetArray) { if (valueNode.equals(targetNode)) { valueFound = true; break; } } if (!valueFound) { targetArray.add(valueNode); } } } else { // Replace the field. parent.set(key, value); } } else { // Replace the field. parent.set(key, value); } } } private static class PathExistsVisitor extends NodeVisitor { private boolean pathPresent = false; @Override JsonNode visitInnerNode(final ObjectNode parent, final String field, final Filter valueFilter) throws ScimException { JsonNode node = parent.path(field); if (node.isArray() && valueFilter != null) { return filterArray((ArrayNode) node, valueFilter, false); } return node; } @Override void visitLeafNode(final ObjectNode parent, final String field, final Filter valueFilter) throws ScimException { JsonNode node = parent.path(field); if (node.isArray() && valueFilter != null) { node = filterArray((ArrayNode) node, valueFilter, false); } if (node.isArray()) { if (node.size() > 0) { setPathPresent(true); } } else if (!node.isMissingNode()) { setPathPresent(true); } } /** * Gets the value of pathPresent. Path present will be set to * true during a traversal if the path was present or false if not. * * @return returns the value of pathPresent */ public boolean isPathPresent() { return pathPresent; } /** * Sets the value of pathPresent. * * @param pathPresent the new value of pathPresent. */ private void setPathPresent(final boolean pathPresent) { this.pathPresent = pathPresent; } } /** * Gets a single value (node) from an ObjectNode at the supplied path. * It is expected that there will only be one matching path. If there * are multiple matching paths (for example a path with filters can * match multiple nodes), an exception will be thrown. * * For example: * With an ObjectNode representing: * { * "name":"Bob", * "favoriteColors":["red","green","blue"] * } * * getValue(Path.fromString("name") * will return a TextNode containing "{@code Bob}" * * getValue(Path.fromString("favoriteColors")) * will return an ArrayNode containing TextNodes with the following * values - "{@code red}", "{@code green}", and "{@code blue}". * * @param path The path to the attributes whose values to retrieve. * @param node the ObjectNode to find the path in. * @return the node located at the path, or a NullNode. * @throws ScimException throw in case of errors. */ public static JsonNode getValue(final Path path, final ObjectNode node) throws ScimException { GatheringNodeVisitor visitor = new GatheringNodeVisitor(false); traverseValues(visitor, node, 0, path); if (visitor.values.isEmpty()) { return NullNode.getInstance(); } else { return visitor.values.get(0); } } /** * Retrieve all JSON nodes referenced by the provided path. If the path * traverses through a JSON array, all nodes the array will be traversed. * For example, given the following ObjectNode: * * <pre> * { * "emails": [ * { * "type": "work", * "value": "bob@work.com" * }, * { * "type": "home", * "value": "bob@home.com" * } * ] * } * </pre> * * Calling getValues with path of emails.value will return a list of all * TextNodes of the "{@code value}" field in the "{@code emails}" array: * * <pre> * [ TextNode("bob@work.com"), TextNode("bob@home.com") ] * </pre> * * However, if the last element of the path references a JSON array, the * entire ArrayNode will returned. For example given the following ObjectNode: * * <pre> * { * "books": [ * { * "title": "Brown Bear, Brown Bear, What Do You See?", * "authors": ["Bill Martin, Jr.", "Eric Carle"] * }, * { * "title": "The Cat In The Hat", * "authors": ["Dr. Seuss"] * } * ] * } * </pre> * * Calling getValues with path of books.authors will return a list of all * ArrayNodes of the "{@code authors}" field in the "{@code books}" array: * * <pre> * [ ArrayNode(["Bill Martin, Jr.", "Eric Carle"]), ArrayNode(["Dr. Seuss"]) ] * </pre> * * @param path The path to the attributes whose values to retrieve. * @param node The JSON node representing the SCIM resource. * * @return List of all JSON nodes referenced by the provided path. * @throws ScimException If an error occurs while traversing the JSON node. */ public static List<JsonNode> findMatchingPaths(final Path path, final ObjectNode node) throws ScimException { GatheringNodeVisitor visitor = new GatheringNodeVisitor(false); traverseValues(visitor, node, 0, path); return visitor.values; } /** * Add a new value at the provided path to the provided JSON node. If the path * contains any value filters, they will be ignored. The following processing * rules are applied depending on the path and value to add: * * <ul> * <li> * If the path is a root path and targets the core or extension * attributes, the value must be a JSON object containing the * set of attributes to be added to the resource. * </li> * <li> * If the path does not exist, the attribute and value is added. * </li> * <li> * If the path targets a complex attribute (an attribute whose value is * a JSON Object), the value must be a JSON object containing the * set of sub-attributes to be added to the complex value. * </li> * <li> * If the path targets a multi-valued attribute (an attribute whose value * if a JSON Array), the value to add must be a JSON array containing the * set of values to be added to the attribute. * </li> * <li> * If the path targets a single-valued attribute, the existing value is * replaced. * </li> * <li> * If the path targets an attribute that does not exist (has not value), * the attribute is added with the new value. * </li> * <li> * If the path targets an existing attribute, the value is replaced. * </li> * <li> * If the path targets an existing attribute which already contains the * value specified, no changes will be made to the node. * </li> * </ul> * * @param path The path to the attribute. * @param node The JSON object node containing the attribute. * @param value The value to add. * @throws ScimException If an error occurs while traversing the JSON node. */ public static void addValue(final Path path, final ObjectNode node, final JsonNode value) throws ScimException { UpdatingNodeVisitor visitor = new UpdatingNodeVisitor(value, true); traverseValues(visitor, node, 0, path); } /** * Remove the value at the provided path. The following processing * rules are applied: * * <ul> * <li> * If the path targets a single-valued attribute, the attribute and its * associated value is removed. * </li> * <li> * If the path targets a multi-valued attribute and no value filter is * specified, the attribute and all values are removed. * </li> * <li> * If the path targets a multi-valued attribute and a value filter is * specified, the values matched by the filter are removed. If after * removal of the selected values, no other values remain, the * multi-valued attribute is removed. * </li> * </ul> * * @param path The path to the attribute. * @param node The JSON object node containing the attribute. * @return The list of nodes that were removed. * @throws ScimException If an error occurs while traversing the JSON node. */ public static List<JsonNode> removeValues(final Path path, final ObjectNode node) throws ScimException { GatheringNodeVisitor visitor = new GatheringNodeVisitor(true); traverseValues(visitor, node, 0, path); return visitor.values; } /** * Update the value at the provided path. The following processing rules are * applied: * * <ul> * <li> * If the path is a root path and targets the core or extension * attributes, the value must be a JSON object containing the * set of attributes to be replaced on the resource. * </li> * <li> * If the path targets a single-valued attribute, the attribute's value * is replaced. * </li> * <li> * If the path targets a multi-valued attribute and no value filter is * specified, the attribute and all values are replaced. * </li> * <li> * If the path targets an attribute that does not exist, treat the * operation as an add. * </li> * <li> * If the path targets a complex attribute (an attribute whose value is * a JSON Object), the value must be a JSON object containing the * set of sub-attributes to be replaced in the complex value. * </li> * <li> * If the path targets a multi-valued attribute and a value filter is * specified that matches one or more values of the multi-valued * attribute, then all matching record values will be replaced. * </li> * <li> * If the path targets a complex multi-valued attribute with a value * filter and a specific sub-attribute * (for example, "addresses[type eq "work"].streetAddress"), the matching * sub-attribute of all matching records is replaced. * </li> * <li> * If the path targets a multi-valued attribute for which a value filter * is specified and no records match was made, the NoTarget exception * will be thrown. * </li> * </ul> * @param path The path to the attribute. * @param node The JSON object node containing the attribute. * @param value The replacement value. * @throws ScimException If an error occurs while traversing the JSON node. */ public static void replaceValue(final Path path, final ObjectNode node, final JsonNode value) throws ScimException { UpdatingNodeVisitor visitor = new UpdatingNodeVisitor(value, false); traverseValues(visitor, node, 0, path); } /** * Checks for the existence of a path. This will return true if the * path is present (even if the value is {@code null}). This allows the caller * to know if the original json string had something like * ... "{@code myPath}":{@code null} ... rather than just leaving the value out of the * json string entirely. * * @param path The path to the attribute. * @param node The JSON object node to search for the path in. * @return true if the path has a value set (even if that value is * set to {@code null}), or false if not. * @throws ScimException If an error occurs while traversing the JSON node. */ public static boolean pathExists(final Path path, final ObjectNode node) throws ScimException { PathExistsVisitor pathExistsVisitor = new PathExistsVisitor(); traverseValues(pathExistsVisitor, node, 0, path); return pathExistsVisitor.isPathPresent(); } /** * Compares two JsonNodes for order. Nodes containing datetime and numerical * values are ordered accordingly. Otherwise, the values' string * representation will be compared lexicographically. * * @param n1 the first node to be compared. * @param n2 the second node to be compared. * @param attributeDefinition The attribute definition of the attribute * whose values to compare or {@code null} to * compare string values using case insensitive * matching. * @return a negative integer, zero, or a positive integer as the * first argument is less than, equal to, or greater than the second. */ public static int compareTo(final JsonNode n1, final JsonNode n2, final AttributeDefinition attributeDefinition) { if (n1.isTextual() && n2.isTextual()) { Date d1 = dateValue(n1); Date d2 = dateValue(n2); if (d1 != null && d2 != null) { return d1.compareTo(d2); } else { if (attributeDefinition != null && attributeDefinition.getType() == AttributeDefinition.Type.STRING && attributeDefinition.isCaseExact()) { return n1.textValue().compareTo(n2.textValue()); } return StaticUtils.toLowerCase(n1.textValue()).compareTo(StaticUtils.toLowerCase(n2.textValue())); } } if (n1.isNumber() && n2.isNumber()) { if (n1.isBigDecimal() || n2.isBigDecimal()) { return n1.decimalValue().compareTo(n2.decimalValue()); } if (n1.isFloatingPointNumber() || n2.isFloatingPointNumber()) { return Double.compare(n1.doubleValue(), n2.doubleValue()); } if (n1.isBigInteger() || n2.isBigInteger()) { return n1.bigIntegerValue().compareTo(n2.bigIntegerValue()); } return Long.compare(n1.longValue(), n2.longValue()); } // Compare everything else lexicographically return n1.asText().compareTo(n2.asText()); } /** * Generates a list of patch operations that can be applied to the source * node in order to make it match the target node. * * @param source The source node for which the set of modifications should * be generated. * @param target The target node, which is what the source node should * look like if the returned modifications are applied. * @param removeMissing Whether to remove fields that are missing in the * target node. * @return A diff with modifications that can be applied to the source * resource in order to make it match the target resource. */ public static List<PatchOperation> diff(final ObjectNode source, final ObjectNode target, final boolean removeMissing) { return new JsonDiff().diff(source, target, removeMissing); } /** * Try to parse out a date from a JSON text node. * * @param node The JSON node to parse. * * @return A parsed date instance or {@code null} if the text is not an * ISO8601 formatted date and time string. */ private static Date dateValue(final JsonNode node) { String text = node.textValue().trim(); if (text.length() >= 19 && Character.isDigit(text.charAt(0)) && Character.isDigit(text.charAt(1)) && Character.isDigit(text.charAt(2)) && Character.isDigit(text.charAt(3)) && text.charAt(4) == '-') { try { return ISO8601Utils.parse(text, new ParsePosition(0)); } catch (ParseException e) { // This is not a date after all. } } return null; } /** * Recursively traver JSON nodes based on a path using the provided node * visitor. * * @param nodeVisitor The NodeVisitor to use to handle the traversed nodes. * @param node The JSON node representing the SCIM resource. * @param path The path to the attributes whose values to retrieve. * * @throws ScimException If an error occurs while traversing the JSON node. */ public static void traverseValues(final NodeVisitor nodeVisitor, final ObjectNode node, final Path path) throws ScimException { traverseValues(nodeVisitor, node, 0, path); } /** * Internal method to recursively gather values based on path. * * @param nodeVisitor The NodeVisitor to use to handle the traversed nodes. * @param node The JSON node representing the SCIM resource. * @param index The index to the current path element. * @param path The path to the attributes whose values to retrieve. * * @throws ScimException If an error occurs while traversing the JSON node. */ private static void traverseValues(final NodeVisitor nodeVisitor, final ObjectNode node, final int index, final Path path) throws ScimException { String field = null; Filter valueFilter = null; int pathDepth = path.size(); if (path.getSchemaUrn() != null) { if (index > 0) { Path.Element element = path.getElement(index - 1); field = element.getAttribute(); valueFilter = element.getValueFilter(); } else { field = path.getSchemaUrn(); } pathDepth += 1; } else if (path.size() > 0) { Path.Element element = path.getElement(index); field = element.getAttribute(); valueFilter = element.getValueFilter(); } if (index < pathDepth - 1) { JsonNode child = nodeVisitor.visitInnerNode(node, field, valueFilter); if (child.isArray()) { for (JsonNode value : child) { if (value.isObject()) { traverseValues(nodeVisitor, (ObjectNode) value, index + 1, path); } } } else if (child.isObject()) { traverseValues(nodeVisitor, (ObjectNode) child, index + 1, path); } } else { nodeVisitor.visitLeafNode(node, field, valueFilter); } } /** * Factory method for constructing a SCIM compatible Jackson * {@link ObjectReader} with default settings. Note that the resulting * instance is NOT usable as is, without defining expected value type with * ObjectReader.forType. * * @return A Jackson {@link ObjectReader} with default settings. */ public static ObjectReader getObjectReader() { return SDK_OBJECT_MAPPER.reader(); } /** * Factory method for constructing a SCIM compatible Jackson * {@link ObjectWriter} with default settings. * * @return A Jackson {@link ObjectWriter} with default settings. */ public static ObjectWriter getObjectWriter() { return SDK_OBJECT_MAPPER.writer(); } /** * Retrieve the SCIM compatible Jackson JsonNodeFactory that may be used * to create tree model JsonNode instances. * * @return The Jackson JsonNodeFactory. */ public static JsonNodeFactory getJsonNodeFactory() { return SDK_OBJECT_MAPPER.getNodeFactory(); } /** * Utility method to convert a POJO to Jackson JSON node. This behaves * exactly the same as Jackson's ObjectMapper.valueToTree. * * @param <T> Actual node type. * @param fromValue POJO to convert. * @return converted JsonNode. */ public static <T extends JsonNode> T valueToNode(final Object fromValue) { return SDK_OBJECT_MAPPER.valueToTree(fromValue); } /** * Utility method to convert Jackson JSON node to a POJO. This behaves * exactly the same as Jackson's ObjectMapper.treeToValue. * * @param <T> Actual node type. * @param fromNode node to convert. * @param valueType The value type. * @return converted POJO. * @throws JsonProcessingException if an error occurs while binding the JSON * node to the value type. */ public static <T> T nodeToValue(final JsonNode fromNode, final Class<T> valueType) throws JsonProcessingException { return SDK_OBJECT_MAPPER.treeToValue(fromNode, valueType); } /** * Utility method to convert Jackson JSON array node to a list of POJOs. * * @param <T> Actual node type. * @param fromNode node to convert. * @param valueType The value type. * @return converted list of POJOs. * @throws JsonProcessingException if an error occurs while binding the JSON * node to the value type. */ public static <T> List<T> nodeToValues(final ArrayNode fromNode, final Class<T> valueType) throws JsonProcessingException { final CollectionType collectionType = SDK_OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, valueType); try { return SDK_OBJECT_MAPPER.readValue(SDK_OBJECT_MAPPER.treeAsTokens(fromNode), collectionType); } catch (JsonProcessingException e) { throw e; } catch (IOException e) { throw new IllegalArgumentException(e.getMessage(), e); } } /** * Creates an configured SCIM compatible Jackson ObjectMapper. Creating new * ObjectMapper instances are expensive so instances should be shared if * possible. Alternatively, consider using one of the getObjectReader, * getObjectWriter, getJsonNodeFactory, or valueToTree methods which uses the * SDK's ObjectMapper singleton. * * @return an Object Mapper with the correct options set for seirializing * and deserializing SCIM JSON objects. */ public static ObjectMapper createObjectMapper() { return mapperFactory.createObjectMapper(); } /** * Sets the MapperFactory used to create the object mappers used by the SCIM 2 * SDK. If this method is called, it should be called prior to the first use * of any other method that may use an ObjectMapper (most of the methods of * JsonUtils use an object mapper). * * @param customMapperFactory the custom JSON object mapper. */ public static void setCustomMapperFactory(final MapperFactory customMapperFactory) { JsonUtils.mapperFactory = customMapperFactory; SDK_OBJECT_MAPPER = customMapperFactory.createObjectMapper(); } }