22
33import java .text .ParseException ;
44import java .text .SimpleDateFormat ;
5+ import java .time .Instant ;
6+ import java .time .OffsetDateTime ;
7+ import java .time .format .DateTimeFormatter ;
8+ import java .time .format .DateTimeFormatterBuilder ;
9+ import java .time .temporal .ChronoField ;
510import java .util .Calendar ;
611import java .util .Date ;
712import java .util .Map ;
813import java .util .TimeZone ;
914import java .util .concurrent .ConcurrentHashMap ;
1015
11- import javax .xml .bind .DatatypeConverter ;
12-
1316/**
1417 * This class provides utility methods for parsing and formatting ISO8601 formatted dates.
1518 */
1619public class ISO8601 {
1720
1821 public static final String PATTERN = "yyyy-MM-dd'T'HH:mm:ssZ" ;
22+ public static final String MSEC_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" ;
1923 public static final String SPACEY_PATTERN = "yyyy-MM-dd HH:mm:ss Z" ;
24+ public static final String SPACEY_MSEC_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS Z" ;
2025 public static final String PATTERN_MSEC = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" ;
2126 public static final String OUTPUT_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'" ;
2227 public static final String OUTPUT_MSEC_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" ;
2328 public static final String UTC_PATTERN = "yyyy-MM-dd HH:mm:ss 'UTC'" ;
2429
25- private static final String PATTERN_REGEX = "\\ d\\ d\\ d\\ d-\\ d\\ d-\\ d\\ dT\\ d\\ d:\\ d\\ d:\\ d\\ d[-+]\\ d\\ d\\ d\\ d" ;
26- private static final String SPACEY_PATTERN_REGEX = "\\ d\\ d\\ d\\ d-\\ d\\ d-\\ d\\ d \\ d\\ d:\\ d\\ d:\\ d\\ d [-+]\\ d\\ d\\ d\\ d" ;
30+ private static final DateTimeFormatter ODT_WITH_MSEC_PARSER = new DateTimeFormatterBuilder ().appendPattern ("yyyy-MM-dd[['T'][ ]HH:mm:ss.SSS[ ][XXXXX][XXXX]]" ).toFormatter ();
31+ private static final DateTimeFormatter ODT_PARSER = new DateTimeFormatterBuilder ().appendPattern ("yyyy-MM-dd[['T'][ ]HH:mm:ss[.SSS][ ][X][XXX]]" )
32+ .parseDefaulting (ChronoField .HOUR_OF_DAY , 0 )
33+ .parseDefaulting (ChronoField .MINUTE_OF_HOUR , 0 )
34+ .parseDefaulting (ChronoField .SECOND_OF_MINUTE , 0 )
35+ .parseDefaulting (ChronoField .MILLI_OF_SECOND , 0 )
36+ .parseDefaulting (ChronoField .OFFSET_SECONDS , 0 )
37+ .toFormatter ();
2738
2839 // Set up ThreadLocal storage to save a thread local SimpleDateFormat keyed with the format string
2940 private static final class SafeDateFormatter {
@@ -116,39 +127,43 @@ public static String toString(Date date) {
116127 }
117128
118129 /**
119- * Parses an ISO8601 formatted string a returns a Date instance.
130+ * Parses an ISO8601 formatted string a returns an Instant instance.
120131 *
121132 * @param dateTimeString the ISO8601 formatted string
122- * @return a Date instance for the ISO8601 formatted string
133+ * @return an Instant instance for the ISO8601 formatted string
123134 * @throws ParseException if the provided string is not in the proper format
124135 */
125- public static Date toDate (String dateTimeString ) throws ParseException {
136+ public static Instant toInstant (String dateTimeString ) throws ParseException {
126137
127138 if (dateTimeString == null ) {
128139 return (null );
129140 }
130141
131142 dateTimeString = dateTimeString .trim ();
132- if (dateTimeString .endsWith ("UTC" )) {
133- return (SafeDateFormatter . getDateFormat ( UTC_PATTERN ) .parse (dateTimeString ));
143+ if (dateTimeString .endsWith ("Z" ) || dateTimeString . endsWith ( " UTC" )) {
144+ return (Instant .parse (dateTimeString ));
134145 } else {
135- try {
136- Calendar cal = DatatypeConverter .parseDateTime (dateTimeString );
137- return (cal .getTime ());
138- } catch (Exception e ) {
139- if (dateTimeString .matches (PATTERN_REGEX )) {
140- // Try using the ISO8601 format
141- return (SafeDateFormatter .getDateFormat (PATTERN ).parse (dateTimeString ));
142- } else if (dateTimeString .matches (SPACEY_PATTERN_REGEX )) {
143- // Try using the invalid ISO8601 format with spaces, GitLab sometimes uses this
144- return (SafeDateFormatter .getDateFormat (SPACEY_PATTERN ).parse (dateTimeString ));
145- } else {
146- throw e ;
147- }
148- }
146+
147+ OffsetDateTime odt = (dateTimeString .length () > 25 ?
148+ OffsetDateTime .parse (dateTimeString , ODT_WITH_MSEC_PARSER ) :
149+ OffsetDateTime .parse (dateTimeString , ODT_PARSER ));
150+
151+ return (odt .toInstant ());
149152 }
150153 }
151154
155+ /**
156+ * Parses an ISO8601 formatted string a returns a Date instance.
157+ *
158+ * @param dateTimeString the ISO8601 formatted string
159+ * @return a Date instance for the ISO8601 formatted string
160+ * @throws ParseException if the provided string is not in the proper format
161+ */
162+ public static Date toDate (String dateTimeString ) throws ParseException {
163+ Instant instant = toInstant (dateTimeString );
164+ return (instant != null ? Date .from (instant ) : null );
165+ }
166+
152167 /**
153168 * Parses an ISO8601 formatted string a returns a Calendar instance.
154169 *
@@ -159,6 +174,10 @@ public static Date toDate(String dateTimeString) throws ParseException {
159174 public static Calendar toCalendar (String dateTimeString ) throws ParseException {
160175
161176 Date date = toDate (dateTimeString );
177+ if (date == null ) {
178+ return (null );
179+ }
180+
162181 Calendar cal = Calendar .getInstance ();
163182 cal .setTime (date );
164183 return (cal );
0 commit comments