diff --git a/core/src/main/java/org/everit/json/schema/internal/DateTimeFormatValidator.java b/core/src/main/java/org/everit/json/schema/internal/DateTimeFormatValidator.java index 063c13494..c63699ee8 100644 --- a/core/src/main/java/org/everit/json/schema/internal/DateTimeFormatValidator.java +++ b/core/src/main/java/org/everit/json/schema/internal/DateTimeFormatValidator.java @@ -15,39 +15,51 @@ */ package org.everit.json.schema.internal; -import org.everit.json.schema.FormatValidator; - -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.util.List; import java.util.Optional; +import com.google.common.collect.ImmutableList; +import org.everit.json.schema.FormatValidator; + /** * Implementation of the "date-time" format value. */ public class DateTimeFormatValidator implements FormatValidator { - private static final String DATETIME_FORMAT_STRING = "yyyy-MM-dd'T'HH:mm:ssXXX"; + private static final List FORMATS_ACCEPTED = ImmutableList.of( + "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}Z" + ); + + private static final String PARTIAL_DATETIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss"; + + private static final String ZONE_OFFSET_PATTERN = "XXX"; + + private static final DateTimeFormatter FORMATTER; + + static { + final DateTimeFormatter secondsFractionFormatter = new DateTimeFormatterBuilder() + .appendFraction(ChronoField.NANO_OF_SECOND, 1, 9, true) + .toFormatter(); - private static final String DATETIME_FORMAT_STRING_SECFRAC = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"; + final DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder() + .appendPattern(PARTIAL_DATETIME_PATTERN) + .appendOptional(secondsFractionFormatter) + .appendPattern(ZONE_OFFSET_PATTERN); - private SimpleDateFormat dateFormat(final String pattern) { - SimpleDateFormat rval = new SimpleDateFormat(pattern); - rval.setLenient(false); - return rval; + FORMATTER = builder.toFormatter(); } @Override public Optional validate(final String subject) { try { - dateFormat(DATETIME_FORMAT_STRING).parse(subject); + FORMATTER.parse(subject); return Optional.empty(); - } catch (ParseException e) { - try { - dateFormat(DATETIME_FORMAT_STRING_SECFRAC).parse(subject); - return Optional.empty(); - } catch (ParseException e1) { - return Optional.of(String.format("[%s] is not a valid date-time", subject)); - } + } catch (DateTimeParseException e) { + return Optional.of(String.format("[%s] is not a valid date-time. Expected %s", subject, FORMATS_ACCEPTED)); } } diff --git a/core/src/test/java/org/everit/json/schema/internal/DefaultFormatValidatorTest.java b/core/src/test/java/org/everit/json/schema/internal/DefaultFormatValidatorTest.java index 67a65ae5f..a0654ea4d 100644 --- a/core/src/test/java/org/everit/json/schema/internal/DefaultFormatValidatorTest.java +++ b/core/src/test/java/org/everit/json/schema/internal/DefaultFormatValidatorTest.java @@ -15,12 +15,12 @@ */ package org.everit.json.schema.internal; +import java.util.Optional; + import org.everit.json.schema.FormatValidator; import org.junit.Assert; import org.junit.Test; -import java.util.Optional; - public class DefaultFormatValidatorTest { private static final String THERE_IS_NO_PLACE_LIKE = "127.0.0.1"; @@ -28,7 +28,7 @@ public class DefaultFormatValidatorTest { private static final String IPV6_ADDR = "2001:db8:85a3:0:0:8a2e:370:7334"; private void assertFailure(final String subject, final FormatValidator format, - final String expectedFailure) { + final String expectedFailure) { Optional opt = format.validate(subject); Assert.assertNotNull("the optional is not null", opt); Assert.assertTrue("failure exists", opt.isPresent()); @@ -44,18 +44,64 @@ private void assertSuccess(final String subject, final FormatValidator format) { @Test public void dateTimeExceedingLimits() { assertFailure("1996-60-999T16:39:57-08:00", new DateTimeFormatValidator(), - "[1996-60-999T16:39:57-08:00] is not a valid date-time"); + "[1996-60-999T16:39:57-08:00] is not a valid date-time. Expected [yyyy-MM-dd'T'HH:mm:ssZ, yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}Z]"); } @Test public void dateTimeFormatFailure() { assertFailure("2015-03-13T11:00:000", new DateTimeFormatValidator(), - "[2015-03-13T11:00:000] is not a valid date-time"); + "[2015-03-13T11:00:000] is not a valid date-time. Expected [yyyy-MM-dd'T'HH:mm:ssZ, yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}Z]"); + } + + @Test + public void dateTimeWithSingleDigitInSecFracSuccess() { + assertSuccess("2015-02-28T11:00:00.1Z", new DateTimeFormatValidator()); + } + + @Test + public void dateTimeWithTwoDigitsInSecFracSuccess() { + assertSuccess("2015-02-28T11:00:00.12Z", new DateTimeFormatValidator()); + } + + @Test + public void dateTimeWithThreeDigitsInSecFracSuccess() { + assertSuccess("2015-02-28T11:00:00.123Z", new DateTimeFormatValidator()); + } + + @Test + public void dateTimeWithFourDigitsInSecFracSuccess() { + assertSuccess("2015-02-28T11:00:00.1234Z", new DateTimeFormatValidator()); + } + + @Test + public void dateTimeWithFiveDigitsInSecFracSuccess() { + assertSuccess("2015-02-28T11:00:00.12345Z", new DateTimeFormatValidator()); + } + + @Test + public void dateTimeWithSixDigitsInSecFracSuccess() { + assertSuccess("2015-02-28T11:00:00.123456Z", new DateTimeFormatValidator()); + } + + @Test + public void dateTimeWithSevenDigitsInSecFracSuccess() { + assertSuccess("2015-02-28T11:00:00.1234567Z", new DateTimeFormatValidator()); + } + + @Test + public void dateTimeWithEightDigitsInSecFracSuccess() { + assertSuccess("2015-02-28T11:00:00.12345678Z", new DateTimeFormatValidator()); + } + + @Test + public void dateTimeWithNineDigitsInSecFracSuccess() { + assertSuccess("2015-02-28T11:00:00.123456789Z", new DateTimeFormatValidator()); } @Test - public void dateTimeSecFracSuccess() { - assertSuccess("2015-02-28T11:00:00.111Z", new DateTimeFormatValidator()); + public void dateTimeWithTenDigitsInSecFracFailure() { + assertFailure("2015-02-28T11:00:00.1234567890Z", new DateTimeFormatValidator(), + "[2015-02-28T11:00:00.1234567890Z] is not a valid date-time. Expected [yyyy-MM-dd'T'HH:mm:ssZ, yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}Z]"); } @Test diff --git a/tests/src/test/resources/org/everit/json/schema/issues/issue56/schema.json b/tests/src/test/resources/org/everit/json/schema/issues/issue56/schema.json new file mode 100644 index 000000000..ed29b385d --- /dev/null +++ b/tests/src/test/resources/org/everit/json/schema/issues/issue56/schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "created": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "created" + ] +} diff --git a/tests/src/test/resources/org/everit/json/schema/issues/issue56/subject-valid.json b/tests/src/test/resources/org/everit/json/schema/issues/issue56/subject-valid.json new file mode 100644 index 000000000..d654b6395 --- /dev/null +++ b/tests/src/test/resources/org/everit/json/schema/issues/issue56/subject-valid.json @@ -0,0 +1,3 @@ +{ + "created": "2011-12-03T10:15:30.1Z" +}