diff --git a/pom.xml b/pom.xml index 90e864e9f..e14286055 100644 --- a/pom.xml +++ b/pom.xml @@ -71,6 +71,7 @@ 2.7.21 2.2 2.2.14.Final + 1.3.0 @@ -94,6 +95,11 @@ true ${version.joni} + + com.ethlo.time + itu + ${version.itu} + ch.qos.logback logback-classic diff --git a/src/main/java/com/networknt/schema/DateTimeValidator.java b/src/main/java/com/networknt/schema/DateTimeValidator.java index 38f98731d..db3d4bae3 100644 --- a/src/main/java/com/networknt/schema/DateTimeValidator.java +++ b/src/main/java/com/networknt/schema/DateTimeValidator.java @@ -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); @@ -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; @@ -66,74 +59,26 @@ public Set 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; } } diff --git a/src/test/java/com/networknt/schema/V4JsonSchemaTest.java b/src/test/java/com/networknt/schema/V4JsonSchemaTest.java index f0d37d3b1..9e757ffe7 100644 --- a/src/test/java/com/networknt/schema/V4JsonSchemaTest.java +++ b/src/test/java/com/networknt/schema/V4JsonSchemaTest.java @@ -42,7 +42,6 @@ public void testLoadingWithId() throws Exception { } @Test - @Disabled public void testFormatDateTimeValidator() throws Exception { runTestFile("draft4/optional/format/date-time.json"); } diff --git a/src/test/resources/draft2019-09/optional/format/date-time.json b/src/test/resources/draft2019-09/optional/format/date-time.json index 11af9acaf..b183a8d02 100644 --- a/src/test/resources/draft2019-09/optional/format/date-time.json +++ b/src/test/resources/draft2019-09/optional/format/date-time.json @@ -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 } ] } diff --git a/src/test/resources/draft7/optional/format/date-time.json b/src/test/resources/draft7/optional/format/date-time.json index 11af9acaf..b183a8d02 100644 --- a/src/test/resources/draft7/optional/format/date-time.json +++ b/src/test/resources/draft7/optional/format/date-time.json @@ -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 } ] }