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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ documentation in the `BulkRequest.java` class, which provides a comprehensive ov
operations are defined and utilized in the SCIM standard. It also includes notes on how to interface
with the new classes, general structures for handling bulk data, useful utility methods, and more.

Updated Jackson to 2.21.2.
Updated Jackson to 2.21.3.

Updated `ErrorResponse.java` to print attributes in an order that is more consistent with the
example JSON objects presented in RFC 7644. Now, `status` is the last attribute printed.
Expand Down Expand Up @@ -57,6 +57,9 @@ method. This provides access to a `JsonMapper.Builder` object that can directly
If your application does not make customizations to the SCIM SDK's object mapper instance, then
there is no change required. See the class-level documentation of `MapperFactory` for more info.

Added new convenience methods to `Meta.java` that accept UNIX timestamps and string location values
to simplify interaction with other data types. Note that UNIX timestamps will use the UTC timezone.

## v5.0.0 - 2025-Dec-15
For consistency with other open source Ping Identity software, the UnboundID SCIM 2 SDK for Java is
now available under the terms of the Apache License (version 2.0). For legacy compatibility, the
Expand Down
17 changes: 9 additions & 8 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,13 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<compileSource>17</compileSource>
<main.basedir>${project.basedir}</main.basedir>
<jackson.version>2.21.2</jackson.version>
<jackson.version>2.21.3</jackson.version>
<jackson-annotations.version>2.21</jackson-annotations.version>
<jakarta-rs.version>4.0.0</jakarta-rs.version>
<jersey.version>4.0.2</jersey.version>
<testng.version>7.12.0</testng.version>
<assertj.version>3.27.7</assertj.version>
<maven.surefire.version>3.5.4</maven.surefire.version>
<maven.surefire.version>3.5.5</maven.surefire.version>
</properties>

<profiles>
Expand All @@ -113,7 +113,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.8</version>
<executions>
<execution>
<id>sign-resources</id>
Expand Down Expand Up @@ -167,7 +166,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<version>3.15.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down Expand Up @@ -208,12 +207,16 @@
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<version>3.5.0</version>
</plugin>
<plugin>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.8</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>buildnumber-maven-plugin</artifactId>
<version>3.2.1</version>
<version>3.3.0</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
Expand Down Expand Up @@ -327,9 +330,7 @@
<configuration>
<tagNameFormat>scim-@{project.version}</tagNameFormat>
<goals>deploy</goals>
<useReleaseProfile>false</useReleaseProfile>
<checkModificationExcludeList>pom.xml</checkModificationExcludeList>
<waitBeforeTagging>3</waitBeforeTagging>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useReleaseProfile is false by default, and the docs for waitBeforeTagging mention that it is useful for SVN repositories, so it's a legacy value that is not needed.

</configuration>
</plugin>
<plugin>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@
* // Set the JSON payload value.
* UserResource user = new UserResource().setUserName("Whiplash!");
* user.setId("5b261bdf");
* user.setMeta(new Meta().setCreated(Calendar.getInstance()));
* user.setMeta(new Meta().setCreatedMillis(Duration.ofHours(13).toMillis()));
* successResult.setResponse(user);
*
* BulkResponse response = new BulkResponse(failedResult, successResult);
Expand Down Expand Up @@ -276,7 +276,7 @@
* try
* {
* UserResource createdUser = validateAndCreateUser(user);
* String location = createdUser.getMeta().getLocation().toString();
* String location = createdUser.getMeta().getLocationString();
* return new BulkOperationResult(op, HTTP_STATUS_CREATED, location);
* }
* catch (ScimException e)
Expand Down Expand Up @@ -390,8 +390,7 @@
* dependencies. If a circular reference is contained within a request via
* {@code bulkId} fields of the {@link BulkOperation} objects, the service
* provider MUST try to resolve it, but MAY stop at a failed attempt with a
* {@link ResourceConflictException}. A simple example of
* a circular bulk request can be seen in
* {@link ResourceConflictException}. An example of this is shown in
* <a href="https://datatracker.ietf.org/doc/html/rfc7644#section-3.7.1">
* RFC 7644 Section 3.7.1</a>. Note that if a SCIM service allows creating loops
* in the graph representation of their groups, the linked example can be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@

package com.unboundid.scim2.common.types;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.unboundid.scim2.common.annotations.Attribute;
import com.unboundid.scim2.common.annotations.NotNull;
import com.unboundid.scim2.common.annotations.Nullable;
import com.unboundid.scim2.common.utils.DateTimeUtils;

import java.net.URI;
import java.util.Calendar;
Expand All @@ -50,10 +52,10 @@
* "id": "fa1afe1",
* "meta": {
* "resourceType": "User",
* "created": "1970-01-01T11:00:00.00Z",
* "created": "1970-01-01T00:00:00.00Z",
* "lastModified": "1970-01-01T11:00:00.00Z",
* "location": "https://example.com/v2/Users/fa1afe1",
* "version": "W/\"e180ee84f0671b1\""
* "version": "e180ee84f0671b1"
* }
* }
* </pre>
Expand All @@ -72,17 +74,18 @@
* any {@link com.unboundid.scim2.common.ScimResource} object:
* <pre><code>
* // Represents January 1st at 11:00 AM.
* final Calendar calendar = Calendar.getInstance();
* calendar.set(getCurrentYear(), Calendar.JANUARY, 1, 11, 0);
* final Calendar lastModified = Calendar.getInstance(ZoneOffset.UTC);
* lastModified.clear();
* lastModified.set(getCurrentYear(), Calendar.JANUARY, 1, 11, 0);
*
* UserResource user = new UserResource();
* user.setId("fa1afe1");
* user.setMeta(new Meta()
* .setResourceType("User")
* .setCreated(calendar)
* .setLastModified(calendar)
* .setLocation(new URI("https://example.com/v2/Users/fa1afe1"))
* .setVersion("W/\"e180ee84f0671b1\"")
* .setCreatedMillis(0L)
* .setLastModified(lastModified)
* .setLocationString("https://example.com/v2/Users/fa1afe1")
* .setVersion("e180ee84f0671b1")
* );
* </code></pre>
*/
Expand Down Expand Up @@ -143,6 +146,22 @@ public Meta setCreated(@Nullable final Calendar created)
return this;
}

/**
* Sets the timestamp of when the SCIM object was created. The timezone will
* be set to UTC.
*
* @param createdMillis The UNIX timestamp representing the time that the SCIM
* resource was created.
* @return This {@code Meta} object.
*
* @since 5.1.0
*/
@NotNull
public Meta setCreatedMillis(final long createdMillis)
{
return setCreated(DateTimeUtils.parse(createdMillis));
}

/**
* Gets the timestamp for the last modification.
*
Expand All @@ -167,6 +186,22 @@ public Meta setLastModified(@Nullable final Calendar lastModified)
return this;
}

/**
* Sets the timestamp of the last modification. The timezone will be set to
* UTC.
*
* @param lastModifiedMillis The UNIX timestamp representing the time when the
* SCIM resource was last updated.
* @return This {@code Meta} object.
*
* @since 5.1.0
*/
@NotNull
public Meta setLastModifiedMillis(final long lastModifiedMillis)
{
return setLastModified(DateTimeUtils.parse(lastModifiedMillis));
}

/**
* Gets the location URI of the SCIM object.
*
Expand All @@ -178,6 +213,18 @@ public URI getLocation()
return location;
}

/**
* Alternative to {@link #getLocation()} which returns a string.
*
* @return The location URI of the SCIM object.
*/
@Nullable
@JsonIgnore
public String getLocationString()
{
return (location == null) ? null : location.toString();
}

/**
* Sets the location URI of the SCIM object.
*
Expand All @@ -191,6 +238,21 @@ public Meta setLocation(@Nullable final URI location)
return this;
}

/**
* Alternative to {@link #setLocation} that accepts a string value.
*
* @param location The location URI of the SCIM object.
* @return This {@code Meta} object.
*
* @throws IllegalArgumentException If the string was not a valid URI.
*/
@NotNull
public Meta setLocationString(@Nullable final String location)
throws IllegalArgumentException
{
return setLocation((location == null) ? null : URI.create(location));
}

/**
* Gets the version of the SCIM object.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@
package com.unboundid.scim2.common;

import com.unboundid.scim2.common.types.Meta;
import com.unboundid.scim2.common.utils.JsonUtils;
import org.testng.annotations.Test;

import java.net.URI;
import java.time.Duration;
import java.util.Calendar;
import java.util.TimeZone;

Expand All @@ -48,30 +50,55 @@
public class MetaTest
{
/**
* Tests that a Meta object can be created with the builder pattern.
* This method provides basic validation for Meta objects.
*/
@Test
public void testBuilderPattern() throws Exception
public void testBasic() throws Exception
{
final Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
calendar.clear();
calendar.set(1970, Calendar.JANUARY, 1, 11, 0);

// The class-level Javadoc of the Meta class contains an example JSON.
// These values are based on the ones from that example.
Meta meta = new Meta()
.setResourceType("User")
.setCreated(calendar)
.setCreatedMillis(0L)
.setLastModified(calendar)
.setLocation(new URI("https://example.com/v2/Users/fa1afe1"))
.setVersion("W/\"e180ee84f0671b1\"");
.setVersion("e180ee84f0671b1");

assertThat(meta.getResourceType()).isEqualTo("User");
assertThat(meta.getCreated()).isEqualTo(calendar);
assertThat(meta.getCreated()).isNotNull();
assertThat(meta.getCreated().getTimeInMillis()).isEqualTo(0L);
assertThat(meta.getLastModified()).isEqualTo(calendar);
assertThat(meta.getLocation()).isNotNull();
assertThat(meta.getLocation().toString())
assertThat(meta.getLocationString())
.isEqualTo("https://example.com/v2/Users/fa1afe1");
assertThat(meta.getVersion()).isEqualTo("W/\"e180ee84f0671b1\"");
assertThat(meta.getVersion()).isEqualTo("e180ee84f0671b1");

// Ensure that the JSON results in an expected form.
String metaJson = JsonUtils.getObjectWriter().writeValueAsString(meta);
String expected = """
{
"resourceType": "User",
"created": "1970-01-01T00:00:00Z",
"lastModified": "1970-01-01T11:00:00Z",
"location": "https://example.com/v2/Users/fa1afe1",
"version": "e180ee84f0671b1"
}""";
String expectedJson = JsonUtils.getObjectReader().readTree(expected)
.toString();
assertThat(metaJson).isEqualTo(expectedJson);

// Ensure the lastModified value can also be set with millisecond values.
long unixTimestamp = Duration.ofHours(11).toMillis();
Meta metaMillis = new Meta()
.setResourceType("User")
.setCreatedMillis(0L)
.setLastModifiedMillis(unixTimestamp)
.setLocationString("https://example.com/v2/Users/fa1afe1")
.setVersion("e180ee84f0671b1");
assertThat(metaMillis).isEqualTo(meta);
}

/**
Expand All @@ -95,4 +122,46 @@ class CustomMeta extends Meta
assertThat(object.getResourceType()).isEqualTo("Custom");
assertThat(object.customField).isEqualTo("present");
}

/**
* Tests for {@code equals()}.
*/
@Test
@SuppressWarnings("all")
public void testEquals()
{
Meta first = new Meta()
.setResourceType("User")
.setCreatedMillis(0L)
.setLastModifiedMillis(10L)
.setLocationString("https://example.com/v2/Users/fa1afe1");
Meta second = new Meta()
.setResourceType("User")
.setCreatedMillis(0L)
.setLastModifiedMillis(10L)
.setLocationString("https://example.com/v2/Users/fa1afe1");

assertThat(first.equals(second)).isTrue();
assertThat(first.equals(first)).isTrue();
assertThat(first.hashCode()).isEqualTo(second.hashCode());
assertThat(first.equals(null)).isFalse();
assertThat(first.equals(new Object())).isFalse();

assertThat(second.setCreatedMillis(10L)).isNotEqualTo(first);
assertThat(second.hashCode()).isNotEqualTo(first.hashCode());
second.setCreatedMillis(first.getCreated().getTimeInMillis());

assertThat(second.setLastModifiedMillis(1000L)).isNotEqualTo(first);
second.setLastModified(first.getLastModified());

assertThat(second.setLocationString(null)).isNotEqualTo(first);
assertThat(second.getLocationString()).isNull();
second.setLocationString(first.getLocationString());

assertThat(second.setResourceType("Other")).isNotEqualTo(first);
second.setResourceType("User");

assertThat(second.setVersion("213")).isNotEqualTo(first);
second.setVersion(null);
}
}
Loading