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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ will use `Uniqueness.NONE`. This class's documentation was also updated to provi
Updated `AttributeDefinition.toString()` to print a JSON model of the object to be consistent with
all other SCIM objects in the library.

Updated `DateTimeUtils.parse()` to support date strings without timestamps (e.g., "1970-01-01").
The time values will always be set to the start of the day.

## 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 @@ -32,21 +32,35 @@
package com.unboundid.scim2.common.utils;

import com.unboundid.scim2.common.annotations.NotNull;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalQueries;
import java.util.Calendar;
import java.util.Date;
import java.util.Objects;
import java.util.TimeZone;

import static java.time.format.DateTimeFormatter.ISO_DATE;
import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;

/**
* Utility methods for handling SCIM 2 DateTime values. The SCIM 2 DateTime
* type is defined as a valid {@code xsd:dateTime} in
* <a href="https://datatracker.ietf.org/doc/html/rfc7643#section-2.3.5">
* RFC 7643, section 2.3.5</a>.
* <br><br>
*
* This class is primarily used during JSON deserialization of date-time values
* within this SDK. However, it may be used as a general utility class to
* perform conversions between date-time strings (e.g., "1970-01-01T00:00:00Z")
* and Java objects. The primary entry points to this class are:
* <ul>
* <li> {@link #parse(String)}: Converts a timestamp string to a Java object.
* <li> {@link #format(Calendar) format()}: Converts an object to a string.
* </ul>
*/
public final class DateTimeUtils
{
Expand Down Expand Up @@ -135,31 +149,21 @@ public static String format(@NotNull final Calendar calendar)
* @param dateStr A SCIM 2 DateTime string.
* @return The DateTime string as a Calendar value.
* @throws IllegalArgumentException if the string cannot be parsed as an
* xsd:dateTime value.
* {@code xsd:dateTime} value.
*/
@NotNull
public static Calendar parse(@NotNull final String dateStr)
throws IllegalArgumentException
{
OffsetDateTime parsedTime;
try
{
parsedTime = OffsetDateTime.parse(dateStr);
}
catch (DateTimeException e)
{
// Re-throw as an IllegalArgumentException for backward compatibility with
// previous releases of the SCIM SDK.
throw new IllegalArgumentException(e);
}
OffsetDateTime parsedTime = asOffsetDateTime(dateStr);

// In previous releases, the default region was "GMT+00:00".
ZoneOffset offset = parsedTime.getOffset();
TimeZone zone = USE_GMT_CALENDARS && offset.equals(ZoneOffset.ofHours(0))
? GMT : TimeZone.getTimeZone(offset);

Calendar calendar = Calendar.getInstance(zone);
calendar.setTime(Date.from(parsedTime.toInstant()));
calendar.setTimeInMillis(parsedTime.toInstant().toEpochMilli());
return calendar;
}

Expand All @@ -180,4 +184,38 @@ public static Calendar parse(final long timestampMillis)
c.setTimeInMillis(timestampMillis);
return c;
}

/**
* Converts the provided string timestamp into a Java object.
*/
@NotNull
private static OffsetDateTime asOffsetDateTime(@NotNull final String dateStr)
throws IllegalArgumentException
{
Exception original;
try
{
// Parse timestamps of the form: 1970-01-01T00:00:00Z
return OffsetDateTime.parse(dateStr);
}
catch (Exception e)
{
original = e;
}

try
{
// Fall back to a date parser without a time: 1970-01-01
TemporalAccessor parsedDate = ISO_DATE.parse(dateStr);
ZoneOffset offset = Objects.requireNonNullElse(
parsedDate.query(TemporalQueries.offset()), ZoneOffset.UTC);

return LocalDate.from(parsedDate).atStartOfDay().atOffset(offset);
}
catch (Exception e)
{
// Use the original exception, as it is more likely to have relevant info.
throw new IllegalArgumentException(original);
}
}
}
Loading
Loading