From 01d2d6488480008e899506f261f5fe467f276257 Mon Sep 17 00:00:00 2001 From: Tuan Dinh Date: Sun, 11 Jun 2017 22:51:49 +1000 Subject: [PATCH 1/2] Use a stricter date-time attribute formatter(rfc3339) --- .../common/RFC3339DateTimeAttribute.java | 68 ++++++++++++++++ .../CommonFormatAttributesDictionary.java | 4 +- .../resources/format/common/date-time.json | 77 +++++++++++++++++-- 3 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/github/fge/jsonschema/format/common/RFC3339DateTimeAttribute.java diff --git a/src/main/java/com/github/fge/jsonschema/format/common/RFC3339DateTimeAttribute.java b/src/main/java/com/github/fge/jsonschema/format/common/RFC3339DateTimeAttribute.java new file mode 100644 index 000000000..7e659b436 --- /dev/null +++ b/src/main/java/com/github/fge/jsonschema/format/common/RFC3339DateTimeAttribute.java @@ -0,0 +1,68 @@ +package com.github.fge.jsonschema.format.common; + +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 com.github.fge.jackson.NodeType; +import com.github.fge.jsonschema.core.exceptions.ProcessingException; +import com.github.fge.jsonschema.core.report.ProcessingReport; +import com.github.fge.jsonschema.format.AbstractFormatAttribute; +import com.github.fge.jsonschema.format.FormatAttribute; +import com.github.fge.jsonschema.processors.data.FullData; +import com.github.fge.msgsimple.bundle.MessageBundle; +import com.google.common.collect.ImmutableList; + +/** + * A {@link DateTimeFormatter} for date and time format defined in RFC3339. + * @see RFC 3339 - Section 5.6 + */ +public class RFC3339DateTimeAttribute extends AbstractFormatAttribute { + + private static final List RFC3339_FORMATS = ImmutableList.of( + "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" + ); + + private static final DateTimeFormatter RFC3339_FORMATTER; + + static { + final DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd") + .appendLiteral('T') + .appendPattern("HH:mm:ss") + .optionalStart() + .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true).parseDefaulting(ChronoField.NANO_OF_SECOND, 0) + .optionalEnd() + .appendOffset("+HH:mm", "Z"); + RFC3339_FORMATTER = builder.toFormatter(); + } + + private static final FormatAttribute INSTANCE = new RFC3339DateTimeAttribute(); + + public static FormatAttribute getInstance() + { + return INSTANCE; + } + + private RFC3339DateTimeAttribute() + { + super("date-time", NodeType.STRING); + } + + @Override + public void validate(final ProcessingReport report, + final MessageBundle bundle, final FullData data) + throws ProcessingException + { + final String value = data.getInstance().getNode().textValue(); + + try { + RFC3339_FORMATTER.parse(value); + } catch (DateTimeParseException ignored) { + report.error(newMsg(data, bundle, "err.format.invalidDate") + .putArgument("value", value).putArgument("expected", RFC3339_FORMATS)); + } + } +} diff --git a/src/main/java/com/github/fge/jsonschema/library/format/CommonFormatAttributesDictionary.java b/src/main/java/com/github/fge/jsonschema/library/format/CommonFormatAttributesDictionary.java index 919758bf9..97e9d884b 100644 --- a/src/main/java/com/github/fge/jsonschema/library/format/CommonFormatAttributesDictionary.java +++ b/src/main/java/com/github/fge/jsonschema/library/format/CommonFormatAttributesDictionary.java @@ -22,9 +22,9 @@ import com.github.fge.jsonschema.core.util.Dictionary; import com.github.fge.jsonschema.core.util.DictionaryBuilder; import com.github.fge.jsonschema.format.FormatAttribute; -import com.github.fge.jsonschema.format.common.DateTimeAttribute; import com.github.fge.jsonschema.format.common.EmailAttribute; import com.github.fge.jsonschema.format.common.IPv6Attribute; +import com.github.fge.jsonschema.format.common.RFC3339DateTimeAttribute; import com.github.fge.jsonschema.format.common.RegexAttribute; import com.github.fge.jsonschema.format.common.URIAttribute; @@ -49,7 +49,7 @@ private CommonFormatAttributesDictionary() FormatAttribute attribute; name = "date-time"; - attribute = DateTimeAttribute.getInstance(); + attribute = RFC3339DateTimeAttribute.getInstance(); builder.addEntry(name, attribute); name = "email"; diff --git a/src/test/resources/format/common/date-time.json b/src/test/resources/format/common/date-time.json index f79dcc701..7b755a5ab 100644 --- a/src/test/resources/format/common/date-time.json +++ b/src/test/resources/format/common/date-time.json @@ -1,6 +1,6 @@ [ { - "data": "2012-12-02T13:05:00+0100", + "data": "2012-12-02T13:05:00+01:00", "valid": true }, { @@ -19,13 +19,60 @@ "data": "2012-08-07T20:42:32.13Z", "valid": true }, + { + "data": "2012-08-07T20:42:32+10:00", + "valid": true + }, + { + "data": "2012-08-07T20:42:32-05:30", + "valid": true + }, + { + "data": "2012-12-02T13:05:00+0100", + "valid": false, + "message": "err.format.invalidDate", + "msgData": { + "value": "2012-12-02T13:05:00+0100", + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] + }, + "msgParams": [ "value", "expected" ] + }, + { + "data": "2012-12-02T13:05:00+0100", + "valid": false, + "message": "err.format.invalidDate", + "msgData": { + "value": "2012-12-02T13:05:00+0100", + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] + }, + "msgParams": [ "value", "expected" ] + }, + { + "data": "2012-12-02T13:05:00Z[Europe/Paris]", + "valid": false, + "message": "err.format.invalidDate", + "msgData": { + "value": "2012-12-02T13:05:00Z[Europe/Paris]", + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] + }, + "msgParams": [ "value", "expected" ] + }, { + "data": "2012-12-02T13:05:00+0100", + "valid": false, + "message": "err.format.invalidDate", + "msgData": { + "value": "2012-12-02T13:05:00+0100", + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] + }, + "msgParams": [ "value", "expected" ] + }, { "data": "2012-02-30T00:00:00+0000", "valid": false, "message": "err.format.invalidDate", "msgData": { "value": "2012-02-30T00:00:00+0000", - "expected": [ "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,12}Z" ] + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] }, "msgParams": [ "value", "expected" ] }, @@ -35,7 +82,7 @@ "message": "err.format.invalidDate", "msgData": { "value": "201202030", - "expected": [ "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,12}Z" ] + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] }, "msgParams": [ "value", "expected" ] }, @@ -69,14 +116,32 @@ }, { "data": "2012-08-07T20:42:32.1234567890Z", - "valid": true + "valid": false, + "message": "err.format.invalidDate", + "msgData": { + "value": "2012-08-07T20:42:32.1234567890Z", + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] + }, + "msgParams": [ "value", "expected" ] }, { "data": "2012-08-07T20:42:32.12345678901Z", - "valid": true + "valid": false, + "message": "err.format.invalidDate", + "msgData": { + "value": "2012-08-07T20:42:32.12345678901Z", + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] + }, + "msgParams": [ "value", "expected" ] }, { "data": "2012-08-07T20:42:32.123456789012Z", - "valid": true + "valid": false, + "message": "err.format.invalidDate", + "msgData": { + "value": "2012-08-07T20:42:32.123456789012Z", + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] + }, + "msgParams": [ "value", "expected" ] } ] From d273e06cc4e604ceb833dc6fae9c8123fb155c1d Mon Sep 17 00:00:00 2001 From: Tuan Dinh Date: Wed, 14 Jun 2017 23:26:43 +1000 Subject: [PATCH 2/2] Used joda-time lib for RFC3339DateTimeAttribute. Added more unit tests --- .../common/RFC3339DateTimeAttribute.java | 80 ++++++++++++----- .../resources/format/common/date-time.json | 89 ++++++++++--------- 2 files changed, 108 insertions(+), 61 deletions(-) diff --git a/src/main/java/com/github/fge/jsonschema/format/common/RFC3339DateTimeAttribute.java b/src/main/java/com/github/fge/jsonschema/format/common/RFC3339DateTimeAttribute.java index 7e659b436..18e73f035 100644 --- a/src/main/java/com/github/fge/jsonschema/format/common/RFC3339DateTimeAttribute.java +++ b/src/main/java/com/github/fge/jsonschema/format/common/RFC3339DateTimeAttribute.java @@ -1,11 +1,11 @@ package com.github.fge.jsonschema.format.common; -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 org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.DateTimeFormatterBuilder; +import org.joda.time.format.DateTimeParser; + import com.github.fge.jackson.NodeType; import com.github.fge.jsonschema.core.exceptions.ProcessingException; import com.github.fge.jsonschema.core.report.ProcessingReport; @@ -22,21 +22,22 @@ public class RFC3339DateTimeAttribute extends AbstractFormatAttribute { private static final List RFC3339_FORMATS = ImmutableList.of( - "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" + "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,12}((+|-)HH:mm|Z)" ); - private static final DateTimeFormatter RFC3339_FORMATTER; + private static final DateTimeFormatter FORMATTER; static { - final DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder() - .appendPattern("yyyy-MM-dd") - .appendLiteral('T') - .appendPattern("HH:mm:ss") - .optionalStart() - .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true).parseDefaulting(ChronoField.NANO_OF_SECOND, 0) - .optionalEnd() - .appendOffset("+HH:mm", "Z"); - RFC3339_FORMATTER = builder.toFormatter(); + final DateTimeParser secFracsParser = new DateTimeFormatterBuilder() + .appendLiteral('.').appendFractionOfSecond(1,12) + .toParser(); + + DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss") + .appendOptional(secFracsParser) + .appendTimeZoneOffset("Z", true, 2, 2); + + FORMATTER = builder.toFormatter(); } private static final FormatAttribute INSTANCE = new RFC3339DateTimeAttribute(); @@ -58,11 +59,50 @@ public void validate(final ProcessingReport report, { final String value = data.getInstance().getNode().textValue(); - try { - RFC3339_FORMATTER.parse(value); - } catch (DateTimeParseException ignored) { - report.error(newMsg(data, bundle, "err.format.invalidDate") - .putArgument("value", value).putArgument("expected", RFC3339_FORMATS)); + try + { + FORMATTER.parseDateTime(value); + + final String secFracsAndOffset = value.substring("yyyy-MM-ddTHH:mm:ss".length()); + final String offset; + if (!secFracsAndOffset.startsWith(".")) { + offset = secFracsAndOffset; + } else{ + if (secFracsAndOffset.contains("Z")) { + offset = secFracsAndOffset.substring(secFracsAndOffset.indexOf("Z")); + } else if (secFracsAndOffset.contains("+")) { + offset = secFracsAndOffset.substring(secFracsAndOffset.indexOf("+")); + } else { + offset = secFracsAndOffset.substring(secFracsAndOffset.indexOf("-")); + } + } + if (!isOffSetStrictRFC3339(offset)) { + throw new IllegalArgumentException(); + } + + } catch (IllegalArgumentException ignored) { + report.error(newMsg(data, bundle, "err.format.invalidDate") + .putArgument("value", value).putArgument("expected", RFC3339_FORMATS)); } + } + + /** + * Return true if date-time offset stricly follows RFC3339: + * time-hour = 2DIGIT ; 00-23 + * time-minute = 2DIGIT ; 00-59 + * time-numoffset = ("+" / "-") time-hour ":" time-minute + * time-offset = "Z" / time-numoffset, + * and false otherwise + * @param offset + * @return + */ + private boolean isOffSetStrictRFC3339(final String offset) + { + if (offset.endsWith("Z")) return true; + if (offset.length() == 6 && offset.contains(":")) { + return true; + } + return false; + } } diff --git a/src/test/resources/format/common/date-time.json b/src/test/resources/format/common/date-time.json index 7b755a5ab..1ee20602d 100644 --- a/src/test/resources/format/common/date-time.json +++ b/src/test/resources/format/common/date-time.json @@ -26,14 +26,14 @@ { "data": "2012-08-07T20:42:32-05:30", "valid": true - }, + }, { - "data": "2012-12-02T13:05:00+0100", + "data": "201202030", "valid": false, "message": "err.format.invalidDate", "msgData": { - "value": "2012-12-02T13:05:00+0100", - "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] + "value": "201202030", + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,12}((+|-)HH:mm|Z)" ] }, "msgParams": [ "value", "expected" ] }, @@ -43,7 +43,17 @@ "message": "err.format.invalidDate", "msgData": { "value": "2012-12-02T13:05:00+0100", - "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,12}((+|-)HH:mm|Z)" ] + }, + "msgParams": [ "value", "expected" ] + }, + { + "data": "2012-12-02T13:05:00+01:30:30", + "valid": false, + "message": "err.format.invalidDate", + "msgData": { + "value": "2012-12-02T13:05:00+01:30:30", + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,12}((+|-)HH:mm|Z)" ] }, "msgParams": [ "value", "expected" ] }, @@ -53,36 +63,47 @@ "message": "err.format.invalidDate", "msgData": { "value": "2012-12-02T13:05:00Z[Europe/Paris]", - "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,12}((+|-)HH:mm|Z)" ] }, "msgParams": [ "value", "expected" ] - }, { - "data": "2012-12-02T13:05:00+0100", + }, + { + "data": "2012-12-02T13:05:00+10:00Z", "valid": false, "message": "err.format.invalidDate", "msgData": { - "value": "2012-12-02T13:05:00+0100", - "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] + "value": "2012-12-02T13:05:00+10:00Z", + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,12}((+|-)HH:mm|Z)" ] }, "msgParams": [ "value", "expected" ] }, { - "data": "2012-02-30T00:00:00+0000", + "data": "2012-12-02T13:05:00America/New_York", "valid": false, "message": "err.format.invalidDate", "msgData": { - "value": "2012-02-30T00:00:00+0000", - "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] + "value": "2012-12-02T13:05:00America/New_York", + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,12}((+|-)HH:mm|Z)" ] }, "msgParams": [ "value", "expected" ] }, { - "data": "201202030", + "data": "2012-12-02T13:05:00[America/New_York]", "valid": false, "message": "err.format.invalidDate", "msgData": { - "value": "201202030", - "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] + "value": "2012-12-02T13:05:00[America/New_York]", + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,12}((+|-)HH:mm|Z)" ] + }, + "msgParams": [ "value", "expected" ] + }, + { + "data": "2012-12-02T13:05:00.123456", + "valid": false, + "message": "err.format.invalidDate", + "msgData": { + "value": "2012-12-02T13:05:00.123456", + "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,12}((+|-)HH:mm|Z)" ] }, "msgParams": [ "value", "expected" ] }, @@ -91,19 +112,19 @@ "valid": true }, { - "data": "2012-08-07T20:42:32.12345Z", + "data": "2012-08-07T20:42:32.1234+05:00", "valid": true }, { - "data": "2012-08-07T20:42:32.123456Z", + "data": "2012-08-07T20:42:32.12345Z", "valid": true }, { - "data": "2012-08-07T20:42:32.1234567Z", + "data": "2012-08-07T20:42:32.123456Z", "valid": true }, { - "data": "2012-08-07T20:42:32.12345678Z", + "data": "2012-08-07T20:42:32.1234567Z", "valid": true }, { @@ -116,32 +137,18 @@ }, { "data": "2012-08-07T20:42:32.1234567890Z", - "valid": false, - "message": "err.format.invalidDate", - "msgData": { - "value": "2012-08-07T20:42:32.1234567890Z", - "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] - }, - "msgParams": [ "value", "expected" ] + "valid": true }, { "data": "2012-08-07T20:42:32.12345678901Z", - "valid": false, - "message": "err.format.invalidDate", - "msgData": { - "value": "2012-08-07T20:42:32.12345678901Z", - "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] - }, - "msgParams": [ "value", "expected" ] + "valid": true }, { "data": "2012-08-07T20:42:32.123456789012Z", - "valid": false, - "message": "err.format.invalidDate", - "msgData": { - "value": "2012-08-07T20:42:32.123456789012Z", - "expected": [ "yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,9}((+|-)HH:mm|Z)" ] - }, - "msgParams": [ "value", "expected" ] + "valid": true + }, + { + "data": "2012-08-07T20:42:32.123456789012+05:00", + "valid": true } ]