Repository: CycloneDX/cyclonedx-core-java
Pull request #588 (2025-01-11 15:24:56)

Support extended ISO 8601 for Date parsing
Support extended ISO 8601 formats including optional milliseconds and offsets


Patch:
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 8b5edc0aa..16c2fcfe5 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -41,7 +41,7 @@ jobs:
         coverage-reports: target/site/jacoco/jacoco.xml
     - name: Upload PR test coverage report
       if: ${{ github.event_name == 'pull_request' }}
-      uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # tag=v4.6.0
+      uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # tag=v4.5.0
       with:
         name: pr-test-coverage-report-java-${{ matrix.java-version }}
         path: target/site/jacoco/jacoco.xml
\ No newline at end of file
diff --git a/src/main/java/org/cyclonedx/util/TimestampUtils.java b/src/main/java/org/cyclonedx/util/TimestampUtils.java
index 6985895cc..fe1800d77 100644
--- a/src/main/java/org/cyclonedx/util/TimestampUtils.java
+++ b/src/main/java/org/cyclonedx/util/TimestampUtils.java
@@ -20,18 +20,22 @@ package org.cyclonedx.util;
 
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 import java.util.Date;
 
 @SuppressWarnings("unused")
 public final class TimestampUtils {
-  private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX");
+  private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.SSS][XXX]");
 
   private TimestampUtils() {}
 
   public static Date parseTimestamp(String text) {
     try {
-      return DATE_FORMAT.parse(text);
-    } catch (ParseException | NullPointerException e) {
+      ZonedDateTime zdt = ZonedDateTime.parse(text, DATE_FORMAT);
+      return Date.from(zdt.toInstant());
+    } catch (DateTimeParseException | NullPointerException e) {
       return null;
     }
   }


Test patch:
diff --git a/src/test/java/org/cyclonedx/util/TimestampUtilsTest.java b/src/test/java/org/cyclonedx/util/TimestampUtilsTest.java
new file mode 100644
index 000000000..c973a27c3
--- /dev/null
+++ b/src/test/java/org/cyclonedx/util/TimestampUtilsTest.java
@@ -0,0 +1,52 @@
+package org.cyclonedx.util;
+
+import java.util.Date;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class TimestampUtilsTest
+{
+  @Test
+  public void testParseTimestampValid() {
+    String validTimestamp = "2023-10-01T12:34:56.789+00:00";
+    Date date = TimestampUtils.parseTimestamp(validTimestamp);
+    assertNotNull(date);
+  }
+
+  @Test
+  public void testParseTimestampWithZ() {
+    String validTimestampWithZ = "2021-01-01T00:00:00.000Z";
+    Date date = TimestampUtils.parseTimestamp(validTimestampWithZ);
+    assertNotNull(date);
+  }
+
+  @Test
+  public void testParseTimestampValidWithoutMilliseconds() {
+    String validTimestamp = "2023-10-01T12:34:56+00:00";
+    Date date = TimestampUtils.parseTimestamp(validTimestamp);
+    assertNotNull(date);
+  }
+
+  @Test
+  public void testParseTimestampInvalid() {
+    String invalidTimestamp = "invalid-timestamp";
+    Date date = TimestampUtils.parseTimestamp(invalidTimestamp);
+    assertNull(date);
+  }
+
+  @Test
+  public void testParseTimestampNull() {
+    Date date = TimestampUtils.parseTimestamp(null);
+    assertNull(date);
+  }
+
+  @Test
+  public void testParseTimestampEmpty() {
+    String emptyTimestamp = "";
+    Date date = TimestampUtils.parseTimestamp(emptyTimestamp);
+    assertNull(date);
+  }
+}