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
}
]