Skip to content

Commit

Permalink
Fix handling of leap seconds in date-time validation (#508) (#524)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Feb 28, 2022
1 parent 53fa5de commit 9af96e8
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 76 deletions.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
<version.mockito>2.7.21</version.mockito>
<version.hamcrest>2.2</version.hamcrest>
<version.undertow>2.2.14.Final</version.undertow>
<version.itu>1.3.0</version.itu>
</properties>
<dependencies>
<dependency>
Expand All @@ -94,6 +95,11 @@
<optional>true</optional>
<version>${version.joni}</version>
</dependency>
<dependency>
<groupId>com.ethlo.time</groupId>
<artifactId>itu</artifactId>
<version>${version.itu}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
Expand Down
95 changes: 20 additions & 75 deletions src/main/java/com/networknt/schema/DateTimeValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,16 @@

package com.networknt.schema;

import com.ethlo.time.ITU;
import com.ethlo.time.LeapSecondException;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

private static final Pattern RFC3339_PATTERN = Pattern.compile(
"^(\\d{4})-(\\d{2})-(\\d{2})" // yyyy-MM-dd
+ "([Tt](\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?" // 'T'HH:mm:ss.milliseconds
+ "(([Zz])|([+-])(\\d{2}):(\\d{2})))?");

public DateTimeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, String formatName) {
super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.DATETIME, validationContext);
this.formatName = formatName;
Expand All @@ -66,74 +59,26 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
}

private boolean isLegalDateTime(String string) {
Matcher matcher = RFC3339_PATTERN.matcher(string);
StringBuilder pattern = new StringBuilder();
StringBuilder dateTime = new StringBuilder();
// Validate the format
if (!matcher.matches()) {
logger.error("Failed to apply RFC3339 pattern on " + string);
return false;
if(formatName.equals(DATE)) {
return tryParse(() -> LocalDate.parse(string));
} else if(formatName.equals(DATETIME)) {
return tryParse(() -> {
try {
ITU.parseDateTime(string);
} catch (LeapSecondException ignored) {}
});
} else {
throw new IllegalStateException("Unknown format: " + formatName);
}
// Validate the date/time content
String year = matcher.group(1);
String month = matcher.group(2);
String day = matcher.group(3);
dateTime.append(year).append('-').append(month).append('-').append(day);
pattern.append("yyyy-MM-dd");

boolean isTimeGiven = matcher.group(4) != null;
boolean isOffsetZuluTime = matcher.group(10) != null;
String hour = null;
String minute = null;
String second = null;
String milliseconds = null;
String timeShiftSign = null;
String timeShiftHour = null;
String timeShiftMinute = null;
}

if (!isTimeGiven && DATETIME.equals(formatName) || (isTimeGiven && DATE.equals(formatName))) {
logger.error("The supplied date/time format type does not match the specification, expected: " + formatName);
private boolean tryParse(Runnable parser) {
try {
parser.run();
return true;
} catch (Exception ex) {
logger.error("Invalid " + formatName + ": " + ex.getMessage());
return false;
}

if (isTimeGiven) {
hour = matcher.group(5);
minute = matcher.group(6);
second = matcher.group(7);
dateTime.append('T').append(hour).append(':').append(minute).append(':').append(second);
pattern.append("'T'HH:mm:ss");
if (matcher.group(8) != null) {
// Normalize milliseconds to 3-length digit
milliseconds = matcher.group(8);
if (milliseconds.length() > 4) {
milliseconds = milliseconds.substring(0, 4);
} else {
while (milliseconds.length() < 4) {
milliseconds += "0";
}
}
dateTime.append(milliseconds);
pattern.append(".SSS");
}

if (isOffsetZuluTime) {
dateTime.append('Z');
pattern.append("'Z'");
} else {
timeShiftSign = matcher.group(11);
timeShiftHour = matcher.group(12);
timeShiftMinute = matcher.group(13);
dateTime.append(timeShiftSign).append(timeShiftHour).append(':').append(timeShiftMinute);
pattern.append("XXX");
}
}
return validateDateTime(dateTime.toString(), pattern.toString());
}

private boolean validateDateTime(String dateTime, String pattern) {
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
sdf.setLenient(false);
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
return sdf.parse(dateTime, new ParsePosition(0)) != null;
}
}
1 change: 0 additions & 1 deletion src/test/java/com/networknt/schema/V4JsonSchemaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ public void testLoadingWithId() throws Exception {
}

@Test
@Disabled
public void testFormatDateTimeValidator() throws Exception {
runTestFile("draft4/optional/format/date-time.json");
}
Expand Down
10 changes: 10 additions & 0 deletions src/test/resources/draft2019-09/optional/format/date-time.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@
"description": "an invalid date-time string without colon in offset",
"data": "1963-06-19T08:30:06+0200",
"valid": false
},
{
"description": "a valid date-time string with leap second",
"data": "1998-12-31T23:59:60Z",
"valid": true
},
{
"description": "an invalid date-time string with leap second",
"data": "1998-10-31T23:59:60Z",
"valid": false
}
]
}
Expand Down
10 changes: 10 additions & 0 deletions src/test/resources/draft7/optional/format/date-time.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@
"description": "an invalid date-time string without colon in offset",
"data": "1963-06-19T08:30:06+0200",
"valid": false
},
{
"description": "a valid date-time string with leap second",
"data": "1998-12-31T23:59:60Z",
"valid": true
},
{
"description": "an invalid date-time string with leap second",
"data": "1998-10-31T23:59:60Z",
"valid": false
}
]
}
Expand Down

0 comments on commit 9af96e8

Please sign in to comment.