Skip to content

Commit 9af96e8

Browse files
aznan2Matti Hanssonmathan
authored
Fix handling of leap seconds in date-time validation (#508) (#524)
* Make date-time validation align with RFC3339 (#508) * Make date-time validation handle leap seconds (#508) Co-authored-by: Matti Hansson <[email protected]> Co-authored-by: mathan <[email protected]>
1 parent 53fa5de commit 9af96e8

File tree

5 files changed

+46
-76
lines changed

5 files changed

+46
-76
lines changed

pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
<version.mockito>2.7.21</version.mockito>
7272
<version.hamcrest>2.2</version.hamcrest>
7373
<version.undertow>2.2.14.Final</version.undertow>
74+
<version.itu>1.3.0</version.itu>
7475
</properties>
7576
<dependencies>
7677
<dependency>
@@ -94,6 +95,11 @@
9495
<optional>true</optional>
9596
<version>${version.joni}</version>
9697
</dependency>
98+
<dependency>
99+
<groupId>com.ethlo.time</groupId>
100+
<artifactId>itu</artifactId>
101+
<version>${version.itu}</version>
102+
</dependency>
97103
<dependency>
98104
<groupId>ch.qos.logback</groupId>
99105
<artifactId>logback-classic</artifactId>

src/main/java/com/networknt/schema/DateTimeValidator.java

+20-75
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,16 @@
1616

1717
package com.networknt.schema;
1818

19+
import com.ethlo.time.ITU;
20+
import com.ethlo.time.LeapSecondException;
1921
import com.fasterxml.jackson.databind.JsonNode;
2022
import org.slf4j.Logger;
2123
import org.slf4j.LoggerFactory;
2224

23-
import java.text.ParsePosition;
24-
import java.text.SimpleDateFormat;
25+
import java.time.LocalDate;
2526
import java.util.Collections;
2627
import java.util.LinkedHashSet;
2728
import java.util.Set;
28-
import java.util.TimeZone;
29-
import java.util.regex.Matcher;
30-
import java.util.regex.Pattern;
3129

3230
public class DateTimeValidator extends BaseJsonValidator implements JsonValidator {
3331
private static final Logger logger = LoggerFactory.getLogger(DateTimeValidator.class);
@@ -38,11 +36,6 @@ public class DateTimeValidator extends BaseJsonValidator implements JsonValidato
3836
private final String DATE = "date";
3937
private final String DATETIME = "date-time";
4038

41-
private static final Pattern RFC3339_PATTERN = Pattern.compile(
42-
"^(\\d{4})-(\\d{2})-(\\d{2})" // yyyy-MM-dd
43-
+ "([Tt](\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?" // 'T'HH:mm:ss.milliseconds
44-
+ "(([Zz])|([+-])(\\d{2}):(\\d{2})))?");
45-
4639
public DateTimeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, String formatName) {
4740
super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.DATETIME, validationContext);
4841
this.formatName = formatName;
@@ -66,74 +59,26 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
6659
}
6760

6861
private boolean isLegalDateTime(String string) {
69-
Matcher matcher = RFC3339_PATTERN.matcher(string);
70-
StringBuilder pattern = new StringBuilder();
71-
StringBuilder dateTime = new StringBuilder();
72-
// Validate the format
73-
if (!matcher.matches()) {
74-
logger.error("Failed to apply RFC3339 pattern on " + string);
75-
return false;
62+
if(formatName.equals(DATE)) {
63+
return tryParse(() -> LocalDate.parse(string));
64+
} else if(formatName.equals(DATETIME)) {
65+
return tryParse(() -> {
66+
try {
67+
ITU.parseDateTime(string);
68+
} catch (LeapSecondException ignored) {}
69+
});
70+
} else {
71+
throw new IllegalStateException("Unknown format: " + formatName);
7672
}
77-
// Validate the date/time content
78-
String year = matcher.group(1);
79-
String month = matcher.group(2);
80-
String day = matcher.group(3);
81-
dateTime.append(year).append('-').append(month).append('-').append(day);
82-
pattern.append("yyyy-MM-dd");
83-
84-
boolean isTimeGiven = matcher.group(4) != null;
85-
boolean isOffsetZuluTime = matcher.group(10) != null;
86-
String hour = null;
87-
String minute = null;
88-
String second = null;
89-
String milliseconds = null;
90-
String timeShiftSign = null;
91-
String timeShiftHour = null;
92-
String timeShiftMinute = null;
73+
}
9374

94-
if (!isTimeGiven && DATETIME.equals(formatName) || (isTimeGiven && DATE.equals(formatName))) {
95-
logger.error("The supplied date/time format type does not match the specification, expected: " + formatName);
75+
private boolean tryParse(Runnable parser) {
76+
try {
77+
parser.run();
78+
return true;
79+
} catch (Exception ex) {
80+
logger.error("Invalid " + formatName + ": " + ex.getMessage());
9681
return false;
9782
}
98-
99-
if (isTimeGiven) {
100-
hour = matcher.group(5);
101-
minute = matcher.group(6);
102-
second = matcher.group(7);
103-
dateTime.append('T').append(hour).append(':').append(minute).append(':').append(second);
104-
pattern.append("'T'HH:mm:ss");
105-
if (matcher.group(8) != null) {
106-
// Normalize milliseconds to 3-length digit
107-
milliseconds = matcher.group(8);
108-
if (milliseconds.length() > 4) {
109-
milliseconds = milliseconds.substring(0, 4);
110-
} else {
111-
while (milliseconds.length() < 4) {
112-
milliseconds += "0";
113-
}
114-
}
115-
dateTime.append(milliseconds);
116-
pattern.append(".SSS");
117-
}
118-
119-
if (isOffsetZuluTime) {
120-
dateTime.append('Z');
121-
pattern.append("'Z'");
122-
} else {
123-
timeShiftSign = matcher.group(11);
124-
timeShiftHour = matcher.group(12);
125-
timeShiftMinute = matcher.group(13);
126-
dateTime.append(timeShiftSign).append(timeShiftHour).append(':').append(timeShiftMinute);
127-
pattern.append("XXX");
128-
}
129-
}
130-
return validateDateTime(dateTime.toString(), pattern.toString());
131-
}
132-
133-
private boolean validateDateTime(String dateTime, String pattern) {
134-
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
135-
sdf.setLenient(false);
136-
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
137-
return sdf.parse(dateTime, new ParsePosition(0)) != null;
13883
}
13984
}

src/test/java/com/networknt/schema/V4JsonSchemaTest.java

-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ public void testLoadingWithId() throws Exception {
4242
}
4343

4444
@Test
45-
@Disabled
4645
public void testFormatDateTimeValidator() throws Exception {
4746
runTestFile("draft4/optional/format/date-time.json");
4847
}

src/test/resources/draft2019-09/optional/format/date-time.json

+10
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@
6464
"description": "an invalid date-time string without colon in offset",
6565
"data": "1963-06-19T08:30:06+0200",
6666
"valid": false
67+
},
68+
{
69+
"description": "a valid date-time string with leap second",
70+
"data": "1998-12-31T23:59:60Z",
71+
"valid": true
72+
},
73+
{
74+
"description": "an invalid date-time string with leap second",
75+
"data": "1998-10-31T23:59:60Z",
76+
"valid": false
6777
}
6878
]
6979
}

src/test/resources/draft7/optional/format/date-time.json

+10
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@
6464
"description": "an invalid date-time string without colon in offset",
6565
"data": "1963-06-19T08:30:06+0200",
6666
"valid": false
67+
},
68+
{
69+
"description": "a valid date-time string with leap second",
70+
"data": "1998-12-31T23:59:60Z",
71+
"valid": true
72+
},
73+
{
74+
"description": "an invalid date-time string with leap second",
75+
"data": "1998-10-31T23:59:60Z",
76+
"valid": false
6777
}
6878
]
6979
}

0 commit comments

Comments
 (0)