Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
Expand Down Expand Up @@ -179,7 +178,7 @@ private Date deserializeToDate(JsonReader in) throws IOException {
}

try {
return ISO8601Utils.parse(s, new ParsePosition(0));
return ISO8601Utils.parseFully(s);
} catch (ParseException e) {
throw new JsonSyntaxException(
"Failed parsing '" + s + "' as Date; at path " + in.getPreviousPath(), e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
*
* @see <a href="http://www.w3.org/TR/NOTE-datetime">this specification</a>
*/
// Date parsing code from Jackson databind ISO8601Utils.java
// Date parsing code from Jackson databind ISO8601Utils.java, with modifications

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just added this as clarification; the code already contained Gson-specific modifications before, see Git history

// https://github.com/FasterXML/jackson-databind/blob/2.8/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
@SuppressWarnings("MemberName") // legacy class name
public final class ISO8601Utils {
Expand Down Expand Up @@ -143,7 +143,11 @@
* @param pos The position to start parsing from, updated to where parsing stopped.
* @return the parsed date
* @throws ParseException if the date is not in the appropriate format
* @deprecated For Gson-internal usage prefer {@link #parseFully(String)}.<br>
* For external users: {@code ISO8601Utils} is a Gson-internal class; prefer {@code java.time}
* for date parsing.
*/
@Deprecated
public static Date parse(String date, ParsePosition pos) throws ParseException {
Exception fail = null;
try {
Expand Down Expand Up @@ -237,14 +241,18 @@
if (timezoneIndicator == 'Z') {
timezone = TIMEZONE_UTC;
offset += 1;

if (date.length() > offset) {
throw new IllegalArgumentException("Trailing data after time zone 'Z'");
}
Comment on lines +245 to +247

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Alternative would be to remove this check here again and let it fail in parseFully instead.

(It seems in all other cases trailing invalid data is already rejected.)

} else if (timezoneIndicator == '+' || timezoneIndicator == '-') {
String timezoneOffset = date.substring(offset);
offset += timezoneOffset.length();

// When timezone has no minutes, we should append it, valid timezones are, for example:
// +00:00, +0000 and +00
timezoneOffset = timezoneOffset.length() >= 5 ? timezoneOffset : timezoneOffset + "00";

offset += timezoneOffset.length();
// 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00"
if (timezoneOffset.equals("+0000") || timezoneOffset.equals("+00:00")) {
timezone = TIMEZONE_UTC;
Expand Down Expand Up @@ -308,6 +316,25 @@
throw ex;
}

/**
* Fully parses a date string.
*
* <p>Delegates to {@link #parse(String, ParsePosition)} and verifies that the given string was
* fully parsed.
*/
public static Date parseFully(String dateStr) throws ParseException {
ParsePosition pos = new ParsePosition(0);
Date date = ISO8601Utils.parse(dateStr, pos);

int parseEnd = pos.getIndex();
if (parseEnd != dateStr.length()) {
// Note: Should be effectively unreachable because `ISO8601Utils#parse` already throws if
// there is unrelated trailing data; but keep this check here to be safe
throw new ParseException("Failed to fully parse date [" + dateStr + "]", parseEnd);
}
return date;
}

/**
* Check if the expected character exist at the given offset in the value.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import static org.junit.Assert.assertThrows;

import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
Expand Down Expand Up @@ -79,28 +78,28 @@ public void testDateFormatWithTimezone() {
@SuppressWarnings("UndefinedEquals")
public void testDateParseWithDefaultTimezone() throws ParseException {
String dateStr = "2018-06-25";
Date date = ISO8601Utils.parse(dateStr, new ParsePosition(0));
Date date = ISO8601Utils.parseFully(dateStr);
Date expectedDate = new GregorianCalendar(2018, Calendar.JUNE, 25).getTime();
assertThat(date).isEqualTo(expectedDate);
}

@Test
public void testDateParseInvalidDay() {
String dateStr = "2022-12-33";
assertThrows(ParseException.class, () -> ISO8601Utils.parse(dateStr, new ParsePosition(0)));
assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr));
}

@Test
public void testDateParseInvalidMonth() {
String dateStr = "2022-14-30";
assertThrows(ParseException.class, () -> ISO8601Utils.parse(dateStr, new ParsePosition(0)));
assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr));
}

@Test
@SuppressWarnings("UndefinedEquals")
public void testDateParseWithTimezone() throws ParseException {
String dateStr = "2018-06-25T00:00:00-03:00";
Date date = ISO8601Utils.parse(dateStr, new ParsePosition(0));
Date date = ISO8601Utils.parseFully(dateStr);
GregorianCalendar calendar = createUtcCalendar();
calendar.set(2018, Calendar.JUNE, 25, 3, 0);
Date expectedDate = calendar.getTime();
Expand All @@ -111,16 +110,55 @@ public void testDateParseWithTimezone() throws ParseException {
@SuppressWarnings("UndefinedEquals")
public void testDateParseSpecialTimezone() throws ParseException {
String dateStr = "2018-06-25T00:02:00-02:58";
Date date = ISO8601Utils.parse(dateStr, new ParsePosition(0));
Date date = ISO8601Utils.parseFully(dateStr);
GregorianCalendar calendar = createUtcCalendar();
calendar.set(2018, Calendar.JUNE, 25, 3, 0);
Date expectedDate = calendar.getTime();
assertThat(date).isEqualTo(expectedDate);
}

@Test
@SuppressWarnings("UndefinedEquals")
public void testDateParseShortZeroTimezone() throws ParseException {
// timezone "+00" is extended by parser to "+0000"
String dateStr = "2018-06-25T00:00:00+00";
Date date = ISO8601Utils.parseFully(dateStr);
GregorianCalendar calendar = createUtcCalendar();
calendar.set(2018, Calendar.JUNE, 25, 0, 0);
Date expectedDate = calendar.getTime();
assertThat(date).isEqualTo(expectedDate);
}

@Test
public void testDateParseInvalidTime() {
String dateStr = "2018-06-25T61:60:62-03:00";
assertThrows(ParseException.class, () -> ISO8601Utils.parse(dateStr, new ParsePosition(0)));
assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr));
}

@Test
public void testDateParseInvalidZTimezone() {
String dateStr = "2018-06-25T00:02:00Z10";
var e = assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr));
assertThat(e)
.hasMessageThat()
.isEqualTo("Failed to parse date [\"" + dateStr + "\"]: Trailing data after time zone 'Z'");
}

@Test
public void testDateParseNoTimezoneIndicator() {
String dateStr = "2018-06-25T00:02:00";
var e = assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr));
assertThat(e)
.hasMessageThat()
.isEqualTo("Failed to parse date [\"" + dateStr + "\"]: No time zone indicator");
}

@Test
public void testDateParseInvalidTimezoneIndicator() {
String dateStr = "2018-06-25T00:02:00X";
var e = assertThrows(ParseException.class, () -> ISO8601Utils.parseFully(dateStr));
assertThat(e)
.hasMessageThat()
.isEqualTo("Failed to parse date [\"" + dateStr + "\"]: Invalid time zone indicator 'X'");
}
}
Loading