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..18e73f035 --- /dev/null +++ b/src/main/java/com/github/fge/jsonschema/format/common/RFC3339DateTimeAttribute.java @@ -0,0 +1,108 @@ +package com.github.fge.jsonschema.format.common; + +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; +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,12}((+|-)HH:mm|Z)" + ); + + private static final DateTimeFormatter FORMATTER; + + static { + 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(); + + 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 + { + 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/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..1ee20602d 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 }, { @@ -20,22 +20,90 @@ "valid": true }, { - "data": "2012-02-30T00:00:00+0000", + "data": "2012-08-07T20:42:32+10:00", + "valid": true + }, + { + "data": "2012-08-07T20:42:32-05:30", + "valid": true + }, + { + "data": "201202030", "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" ] + "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" ] }, { - "data": "201202030", + "data": "2012-12-02T13:05:00+0100", "valid": false, "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" ] + "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,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" ] + }, + { + "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,12}((+|-)HH:mm|Z)" ] + }, + "msgParams": [ "value", "expected" ] + }, + { + "data": "2012-12-02T13:05:00+10:00Z", + "valid": false, + "message": "err.format.invalidDate", + "msgData": { + "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-12-02T13:05:00America/New_York", + "valid": false, + "message": "err.format.invalidDate", + "msgData": { + "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": "2012-12-02T13:05:00[America/New_York]", + "valid": false, + "message": "err.format.invalidDate", + "msgData": { + "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" ] }, @@ -44,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 }, { @@ -78,5 +146,9 @@ { "data": "2012-08-07T20:42:32.123456789012Z", "valid": true + }, + { + "data": "2012-08-07T20:42:32.123456789012+05:00", + "valid": true } ]