Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,27 @@ The time values will always be set to the start of the day.
Updated the `@Attribute` annotation of all `$ref` fields to label them as not required for
consistency across groups, devices, and other model classes.

Deprecated the following parameters. They will be removed in a future release. If you do not modify
these, there is no change to make.
* `BulkResourceMapper.java`: Renamed to `ScimResourceMapper.java` for use in non-bulk workflows.

* `BaseScimResource.IGNORE_UNKNOWN_FIELDS`: This system property modifies behavior when a JSON
string is transformed into a Java object. If the JSON value contains extra fields that cannot
be stored on the object, then an exception will be thrown unless this property is `true` (the
current default as of 6.0.0). However, as we've stated elsewhere, adding fields to an API response
is not considered a breaking change by convention. The SCIM standard has done this several times
recently, such as adding `nextCursor` to a `ListResponse` in RFC 9944. In other words, a client
written with an old release of the SCIM SDK could have problems parsing valid SCIM responses from
the future if this property is `false`. To avoid brittle behavior, it is imperative that unknown
JSON fields are always ignored. Thus, this property will be removed for better interoperability.

* `DateTimeUtils.USE_GMT_CALENDARS`: DateTimeUtils initially used JAX-B for date-time processing,
mostly for Java 7 support. JAX-B's Calendar objects always used a "GMT+00:00" timezone by default,
but Calendar objects in Java are much more likely to use UTC instead. Furthermore, UTC was
designed for date-time synchronization, so it is a better timezone to use for the default region.
JAX-B was removed in 5.0.0, but GMT support was kept with a system property for legacy uses. For
consistency, we intend to remove this property, as UTC has been in use by default since 6.0.0.

## 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,20 @@ public abstract class BaseScimResource
* ignored by default. For alignment with Jackson, this property is set to
* {@code true} (as of version 6.0.0) so that unknown fields in the JSON
* representation of a BaseScimResource are also ignored (if they are not a
* schema extension). It is strongly encouraged to avoid changing this
* setting, as updates to the SCIM standard can cause new fields to be added
* (e.g., RFC 9865 added fields like {@code nextCursor} to ListResponse).
* schema extension).
*
* @since 4.0.0
* @deprecated The SCIM standard has been updated after the publication of
* RFC 7644, adding new fields to existing JSON models. Thus, an
* unknown JSON field (e.g., {@code nextCursor}) could be a new,
* legitimate SCIM value that is not supported by the application
* yet, so this case should never result in an error. Further,
* adding new fields is not considered a breaking change by HTTP
* convention. To encourage better forward-compatibility, callers
* who have set this property to {@code false} should avoid being
* strict on unknown properties/fields.
*/
@Deprecated(since = "6.1.0")
public static boolean IGNORE_UNKNOWN_FIELDS = StaticUtils.getProperty(
"com.unboundid.scim2.common.BaseScimResource.ignoreUnknownFields", true);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
/*
* Copyright 2026 Ping Identity Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copyright 2026 Ping Identity Corporation
*
* 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;

import com.unboundid.scim2.common.annotations.NotNull;
import com.unboundid.scim2.common.annotations.Nullable;
import com.unboundid.scim2.common.annotations.Schema;
import com.unboundid.scim2.common.messages.ErrorResponse;
import com.unboundid.scim2.common.messages.PatchRequest;
import com.unboundid.scim2.common.types.GroupResource;
import com.unboundid.scim2.common.types.UserResource;
import com.unboundid.scim2.common.utils.JsonUtils;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;


/**
* This class is used to simplify the process of obtaining SCIM resources that
* are nested within a JSON payload.
* <br><br>
*
* An example of this structure is in bulk responses, where the value of the
* {@code response} field below represents a user that was just created:
* <pre>
* {
* "location": "/Users/fa1afe1",
* "method": "POST",
* "status": "200",
* "response": {
* "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User" ],
* "id": "fa1afe1",
* "userName": "Polaroid"
* }
* }
* </pre>
* <br><br>
*
* When it comes to handling and interpreting this data, it can be tedious to
* obtain the resource and convert it from JSON to a usable POJO. This is
* because the data in the {@code response} field may correspond to:
* <ul>
* <li> A {@link UserResource}, representing a user.
* <li> A {@link GroupResource}, representing a group entity.
* <li> A {@link ErrorResponse}, representing an error that occurred.
* <li> Any other custom resource type defined by a SCIM service provider.
* </ul>
* <br><br>
*
* The definition of any SCIM resource type is available in the {@code schemas}
* attribute. However, fetching this data and conditionally converting it into
* different objects (with {@link JsonUtils#nodeToValue}) is extra work that the
* SCIM SDK aims to simplify.
* <br><br>
*
* To solve this problem, this ScimResourceMapper class defines associations
* between schemas (represented as sets) and a Java type. For example, any JSON
* with a schemas value of {@code urn:ietf:params:scim:schemas:core:2.0:User}
* is interpreted as a {@link UserResource}. If you have custom resource types
* that need to be supported by the SCIM SDK's processing, then add it with
* one of the following methods.
* <ul>
* <li> {@link #add(Class)}: If the class uses the {@link Schema} annotation.
* <li> {@link #put put()}: To define a direct relationship for any class.
* </ul>
* <br><br>
*
* Note that any custom class must descend from {@link BaseScimResource} or
* {@link GenericScimResource} in order to be compatible with the methods above.
* Furthermore, modifying the ScimResourceMapper with the above methods must
* only be done at application startup.
*/
public class ScimResourceMapper
{
/**
* The map that stores schema-to-class associations for this mapper class.
*/
// This does not need to be a concurrent map since it should only be
// configured/modified at application startup.
@NotNull
static final HashMap<Set<String>, Class<?>> SCHEMAS_MAP = new HashMap<>();

static
{
initialize();
}

/**
* Updates the ScimResourceMapper with a new class. The provided class must
* include the {@link Schema} annotation. To add a different
* ScimResource-based class that does not use the annotation, use the
* {@link #put put()} method instead.
*
* @param clazz The class to register.
* @param <T> The Java type, which must implement ScimResource.
* @throws IllegalArgumentException If the provided class does not have the
* required annotation.
*/
public static synchronized <T extends ScimResource> void add(
@NotNull final Class<T> clazz)
throws IllegalArgumentException
{
Schema schema = clazz.getAnnotation(Schema.class);
if (schema == null)
{
throw new IllegalArgumentException("Requested schema for the "
+ clazz.getName()
+ " class, which does not have a valid @Schema annotation.");
}

SCHEMAS_MAP.put(Set.of(schema.id()), clazz);
}

/**
* Updates the ScimResourceMapper with a new class. This is an alternative to
* the {@link #add(Class)} method that supports usage of Java classes that
* do not or cannot use the {@code @Schema} annotation.
*
* @param schemas The schemas associated with the resource type.
* @param clazz The class type that is associated with the resource type.
* @param <T> The returned Java type.
*/
public static synchronized <T extends ScimResource> void put(
@NotNull final Set<String> schemas,
@NotNull final Class<T> clazz)
{
Objects.requireNonNull(schemas);
SCHEMAS_MAP.put(schemas, clazz);
}

/**
* Clears all registered relationships between schemas and class types.
* <br><br>
*
* This should generally not be called unless it is absolutely necessary to
* overwrite data in the map.
*/
public static void clear()
{
SCHEMAS_MAP.clear();
}

/**
* This utility method is the primary entrypoint to this class, and is
* responsible for converting JSON data into a ScimResource POJO. The subclass
* of ScimResource must be defined in the map contained within this class, or
* a {@link GenericScimResource} will be returned instead.
*
* @param json The JSON to convert.
* @return A {@link ScimResource} subclass, or {@code null} if the JSON
* was also {@code null}.
*
* @throws IllegalArgumentException If the JSON was malformed.
*/
@Nullable
public static ScimResource asScimResource(@Nullable final ObjectNode json)
throws IllegalArgumentException
{
if (json == null)
{
return null;
}

// Attempt fetching the class using data from the "schemas" array. If there
// is not a mapping, a GenericScimResource will be used.
Class<ScimResource> clazz = ScimResourceMapper.get(json.get("schemas"));

try
{
return JsonUtils.nodeToValue(json, clazz);
}
catch (JacksonException e)
{
throw new IllegalArgumentException(
"Failed to convert SCIM data into a " + clazz.getName(), e);
}
}

/**
* Fetches the class associated with the provided schema(s). If the schemas
* are not registered, a {@link GenericScimResource} is returned.
*
* @param s The {@code schemas} value of a SCIM resource.
* @param <T> The returned Java type.
*
* @return The class type that is associated with the schemas, or a
* {@link GenericScimResource} if one is not defined.
*/
@NotNull
@SuppressWarnings("unchecked")
static <T extends ScimResource> Class<T> get(@NotNull final Set<String> s)
{
return (Class<T>) SCHEMAS_MAP.getOrDefault(s, GenericScimResource.class);
}

/**
* Fetches the class associated with the provided schema set. This is an
* alternative to {@link #get(Set)} which accepts a JsonNode.
*
* @param node The JsonNode representing the value of the {@code schemas}
* field on a SCIM resource.
* @param <T> The returned Java type.
*
* @return The class type that is associated with the schemas, or a
* {@link GenericScimResource} if one is not defined.
*/
@NotNull
@SuppressWarnings("unchecked")
static <T extends ScimResource> Class<T> get(@Nullable final JsonNode node)
{
if (!(node instanceof ArrayNode arrayNode))
{
// 'schemas' should always be an array as defined by the SCIM standard.
return (Class<T>) GenericScimResource.class;
}

Set<String> schemaSet = new HashSet<>();
for (var value : arrayNode)
{
schemaSet.add(value.asString());
}

return get(schemaSet);
}

/**
* Initializes this class by registering basic resource types that are
* generally applicable to SCIM clients.
*/
static void initialize()
{
clear();
add(UserResource.class);
add(GroupResource.class);
add(ErrorResponse.class);
add(PatchRequest.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import com.unboundid.scim2.common.GenericScimResource;
import com.unboundid.scim2.common.Path;
import com.unboundid.scim2.common.ScimResource;
import com.unboundid.scim2.common.ScimResourceMapper;
import com.unboundid.scim2.common.annotations.NotNull;
import com.unboundid.scim2.common.annotations.Nullable;
import com.unboundid.scim2.common.exceptions.runtime.BulkRequestException;
Expand Down Expand Up @@ -354,7 +355,7 @@ public ObjectNode getData()
* <br><br>
*
* If you have custom classes that should be used, they may be registered with
* the {@link BulkResourceMapper}. However, any JSON object may be manually
* the {@link ScimResourceMapper}. However, any JSON object may be manually
* converted to a Java object with {@link JsonUtils#nodeToValue}:
* <pre><code>
* ObjectNode data = bulkOperation.getData();
Expand All @@ -380,7 +381,7 @@ public ScimResource getDataAsScimResource()
{
try
{
return BulkResourceMapper.asScimResource(data);
return ScimResourceMapper.asScimResource(data);
}
catch (IllegalArgumentException e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.unboundid.scim2.common.ScimResource;
import com.unboundid.scim2.common.ScimResourceMapper;
import com.unboundid.scim2.common.annotations.NotNull;
import com.unboundid.scim2.common.annotations.Nullable;
import com.unboundid.scim2.common.exceptions.ScimException;
Expand Down Expand Up @@ -561,7 +562,7 @@ public ScimResource getResponseAsScimResource()
{
try
{
return BulkResourceMapper.asScimResource(response);
return ScimResourceMapper.asScimResource(response);
}
catch (IllegalArgumentException e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.unboundid.scim2.common.BaseScimResource;
import com.unboundid.scim2.common.ScimResource;
import com.unboundid.scim2.common.ScimResourceMapper;
import com.unboundid.scim2.common.annotations.Attribute;
import com.unboundid.scim2.common.annotations.NotNull;
import com.unboundid.scim2.common.annotations.Nullable;
Expand Down Expand Up @@ -315,7 +316,7 @@
* }
* </code></pre>
*
* The {@link BulkResourceMapper} is responsible for managing the conversions to
* The {@link ScimResourceMapper} is responsible for managing the conversions to
* {@link ScimResource} objects. See the mapper and
* {@link BulkOperation#getDataAsScimResource()} for more details.
* <br><br>
Expand Down
Loading
Loading