Skip to content

Commit 728b3da

Browse files
authored
Merge pull request #235 from tvdinh/rfc3339_datetime
Use a stricter date-time attribute formatter(rfc3339)
2 parents c1fffae + d273e06 commit 728b3da

File tree

3 files changed

+193
-13
lines changed

3 files changed

+193
-13
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package com.github.fge.jsonschema.format.common;
2+
3+
import java.util.List;
4+
5+
import org.joda.time.format.DateTimeFormatter;
6+
import org.joda.time.format.DateTimeFormatterBuilder;
7+
import org.joda.time.format.DateTimeParser;
8+
9+
import com.github.fge.jackson.NodeType;
10+
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
11+
import com.github.fge.jsonschema.core.report.ProcessingReport;
12+
import com.github.fge.jsonschema.format.AbstractFormatAttribute;
13+
import com.github.fge.jsonschema.format.FormatAttribute;
14+
import com.github.fge.jsonschema.processors.data.FullData;
15+
import com.github.fge.msgsimple.bundle.MessageBundle;
16+
import com.google.common.collect.ImmutableList;
17+
18+
/**
19+
* A {@link DateTimeFormatter} for date and time format defined in RFC3339.
20+
* @see <a href="https://tools.ietf.org/html/rfc3339#section-5.6">RFC 3339 - Section 5.6</a>
21+
*/
22+
public class RFC3339DateTimeAttribute extends AbstractFormatAttribute {
23+
24+
private static final List<String> RFC3339_FORMATS = ImmutableList.of(
25+
"yyyy-MM-dd'T'HH:mm:ss((+|-)HH:mm|Z)", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,12}((+|-)HH:mm|Z)"
26+
);
27+
28+
private static final DateTimeFormatter FORMATTER;
29+
30+
static {
31+
final DateTimeParser secFracsParser = new DateTimeFormatterBuilder()
32+
.appendLiteral('.').appendFractionOfSecond(1,12)
33+
.toParser();
34+
35+
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder()
36+
.appendPattern("yyyy-MM-dd'T'HH:mm:ss")
37+
.appendOptional(secFracsParser)
38+
.appendTimeZoneOffset("Z", true, 2, 2);
39+
40+
FORMATTER = builder.toFormatter();
41+
}
42+
43+
private static final FormatAttribute INSTANCE = new RFC3339DateTimeAttribute();
44+
45+
public static FormatAttribute getInstance()
46+
{
47+
return INSTANCE;
48+
}
49+
50+
private RFC3339DateTimeAttribute()
51+
{
52+
super("date-time", NodeType.STRING);
53+
}
54+
55+
@Override
56+
public void validate(final ProcessingReport report,
57+
final MessageBundle bundle, final FullData data)
58+
throws ProcessingException
59+
{
60+
final String value = data.getInstance().getNode().textValue();
61+
62+
try
63+
{
64+
FORMATTER.parseDateTime(value);
65+
66+
final String secFracsAndOffset = value.substring("yyyy-MM-ddTHH:mm:ss".length());
67+
final String offset;
68+
if (!secFracsAndOffset.startsWith(".")) {
69+
offset = secFracsAndOffset;
70+
} else{
71+
if (secFracsAndOffset.contains("Z")) {
72+
offset = secFracsAndOffset.substring(secFracsAndOffset.indexOf("Z"));
73+
} else if (secFracsAndOffset.contains("+")) {
74+
offset = secFracsAndOffset.substring(secFracsAndOffset.indexOf("+"));
75+
} else {
76+
offset = secFracsAndOffset.substring(secFracsAndOffset.indexOf("-"));
77+
}
78+
}
79+
if (!isOffSetStrictRFC3339(offset)) {
80+
throw new IllegalArgumentException();
81+
}
82+
83+
} catch (IllegalArgumentException ignored) {
84+
report.error(newMsg(data, bundle, "err.format.invalidDate")
85+
.putArgument("value", value).putArgument("expected", RFC3339_FORMATS));
86+
}
87+
88+
}
89+
90+
/**
91+
* Return true if date-time offset stricly follows RFC3339:
92+
* <code>time-hour = 2DIGIT ; 00-23</code>
93+
* <code>time-minute = 2DIGIT ; 00-59</code>
94+
* <code>time-numoffset = ("+" / "-") time-hour ":" time-minute</code>
95+
* <code>time-offset = "Z" / time-numoffset</code>,
96+
* and false otherwise
97+
* @param offset
98+
* @return
99+
*/
100+
private boolean isOffSetStrictRFC3339(final String offset)
101+
{
102+
if (offset.endsWith("Z")) return true;
103+
if (offset.length() == 6 && offset.contains(":")) {
104+
return true;
105+
}
106+
return false;
107+
}
108+
}

src/main/java/com/github/fge/jsonschema/library/format/CommonFormatAttributesDictionary.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
import com.github.fge.jsonschema.core.util.Dictionary;
2323
import com.github.fge.jsonschema.core.util.DictionaryBuilder;
2424
import com.github.fge.jsonschema.format.FormatAttribute;
25-
import com.github.fge.jsonschema.format.common.DateTimeAttribute;
2625
import com.github.fge.jsonschema.format.common.EmailAttribute;
2726
import com.github.fge.jsonschema.format.common.IPv6Attribute;
27+
import com.github.fge.jsonschema.format.common.RFC3339DateTimeAttribute;
2828
import com.github.fge.jsonschema.format.common.RegexAttribute;
2929
import com.github.fge.jsonschema.format.common.URIAttribute;
3030

@@ -49,7 +49,7 @@ private CommonFormatAttributesDictionary()
4949
FormatAttribute attribute;
5050

5151
name = "date-time";
52-
attribute = DateTimeAttribute.getInstance();
52+
attribute = RFC3339DateTimeAttribute.getInstance();
5353
builder.addEntry(name, attribute);
5454

5555
name = "email";

src/test/resources/format/common/date-time.json

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[
22
{
3-
"data": "2012-12-02T13:05:00+0100",
3+
"data": "2012-12-02T13:05:00+01:00",
44
"valid": true
55
},
66
{
@@ -20,22 +20,90 @@
2020
"valid": true
2121
},
2222
{
23-
"data": "2012-02-30T00:00:00+0000",
23+
"data": "2012-08-07T20:42:32+10:00",
24+
"valid": true
25+
},
26+
{
27+
"data": "2012-08-07T20:42:32-05:30",
28+
"valid": true
29+
},
30+
{
31+
"data": "201202030",
2432
"valid": false,
2533
"message": "err.format.invalidDate",
2634
"msgData": {
27-
"value": "2012-02-30T00:00:00+0000",
28-
"expected": [ "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,12}Z" ]
35+
"value": "201202030",
36+
"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)" ]
2937
},
3038
"msgParams": [ "value", "expected" ]
3139
},
3240
{
33-
"data": "201202030",
41+
"data": "2012-12-02T13:05:00+0100",
3442
"valid": false,
3543
"message": "err.format.invalidDate",
3644
"msgData": {
37-
"value": "201202030",
38-
"expected": [ "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss.[0-9]{1,12}Z" ]
45+
"value": "2012-12-02T13:05:00+0100",
46+
"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)" ]
47+
},
48+
"msgParams": [ "value", "expected" ]
49+
},
50+
{
51+
"data": "2012-12-02T13:05:00+01:30:30",
52+
"valid": false,
53+
"message": "err.format.invalidDate",
54+
"msgData": {
55+
"value": "2012-12-02T13:05:00+01:30:30",
56+
"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)" ]
57+
},
58+
"msgParams": [ "value", "expected" ]
59+
},
60+
{
61+
"data": "2012-12-02T13:05:00Z[Europe/Paris]",
62+
"valid": false,
63+
"message": "err.format.invalidDate",
64+
"msgData": {
65+
"value": "2012-12-02T13:05:00Z[Europe/Paris]",
66+
"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)" ]
67+
},
68+
"msgParams": [ "value", "expected" ]
69+
},
70+
{
71+
"data": "2012-12-02T13:05:00+10:00Z",
72+
"valid": false,
73+
"message": "err.format.invalidDate",
74+
"msgData": {
75+
"value": "2012-12-02T13:05:00+10:00Z",
76+
"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)" ]
77+
},
78+
"msgParams": [ "value", "expected" ]
79+
},
80+
{
81+
"data": "2012-12-02T13:05:00America/New_York",
82+
"valid": false,
83+
"message": "err.format.invalidDate",
84+
"msgData": {
85+
"value": "2012-12-02T13:05:00America/New_York",
86+
"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)" ]
87+
},
88+
"msgParams": [ "value", "expected" ]
89+
},
90+
{
91+
"data": "2012-12-02T13:05:00[America/New_York]",
92+
"valid": false,
93+
"message": "err.format.invalidDate",
94+
"msgData": {
95+
"value": "2012-12-02T13:05:00[America/New_York]",
96+
"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)" ]
97+
},
98+
"msgParams": [ "value", "expected" ]
99+
},
100+
{
101+
"data": "2012-12-02T13:05:00.123456",
102+
"valid": false,
103+
"message": "err.format.invalidDate",
104+
"msgData": {
105+
"value": "2012-12-02T13:05:00.123456",
106+
"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)" ]
39107
},
40108
"msgParams": [ "value", "expected" ]
41109
},
@@ -44,19 +112,19 @@
44112
"valid": true
45113
},
46114
{
47-
"data": "2012-08-07T20:42:32.12345Z",
115+
"data": "2012-08-07T20:42:32.1234+05:00",
48116
"valid": true
49117
},
50118
{
51-
"data": "2012-08-07T20:42:32.123456Z",
119+
"data": "2012-08-07T20:42:32.12345Z",
52120
"valid": true
53121
},
54122
{
55-
"data": "2012-08-07T20:42:32.1234567Z",
123+
"data": "2012-08-07T20:42:32.123456Z",
56124
"valid": true
57125
},
58126
{
59-
"data": "2012-08-07T20:42:32.12345678Z",
127+
"data": "2012-08-07T20:42:32.1234567Z",
60128
"valid": true
61129
},
62130
{
@@ -78,5 +146,9 @@
78146
{
79147
"data": "2012-08-07T20:42:32.123456789012Z",
80148
"valid": true
149+
},
150+
{
151+
"data": "2012-08-07T20:42:32.123456789012+05:00",
152+
"valid": true
81153
}
82154
]

0 commit comments

Comments
 (0)