Skip to content

Commit dd9970b

Browse files
authored
Merge pull request #149 from networknt/fix/#138-date-validation
Added DateTimeValidator
2 parents ccb1851 + 466099a commit dd9970b

File tree

4 files changed

+377
-151
lines changed

4 files changed

+377
-151
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright (c) 2016 Network New Technologies Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.networknt.schema;
18+
19+
import com.fasterxml.jackson.databind.JsonNode;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
23+
import java.text.ParsePosition;
24+
import java.text.SimpleDateFormat;
25+
import java.util.Collections;
26+
import java.util.LinkedHashSet;
27+
import java.util.Set;
28+
import java.util.regex.Matcher;
29+
import java.util.regex.Pattern;
30+
31+
public class DateTimeValidator extends BaseJsonValidator implements JsonValidator {
32+
private static final Logger logger = LoggerFactory.getLogger(DateTimeValidator.class);
33+
34+
private final String formatName;
35+
private final String DATE = "date";
36+
private final String DATETIME = "date-time";
37+
38+
private static final Pattern RFC3339_PATTERN = Pattern.compile(
39+
"^(\\d{4})-(\\d{2})-(\\d{2})" // yyyy-MM-dd
40+
+ "([Tt](\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?)?" // 'T'HH:mm:ss.milliseconds
41+
+ "([Zz]|([+-])(\\d{2}):(\\d{2}))?");
42+
43+
public DateTimeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, String formatName) {
44+
super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.DATETIME, validationContext);
45+
this.formatName = formatName;
46+
parseErrorCode(getValidatorType().getErrorCodeKey());
47+
}
48+
49+
public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
50+
debug(logger, node, rootNode, at);
51+
52+
Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
53+
54+
JsonType nodeType = TypeFactory.getValueNodeType(node);
55+
if (nodeType != JsonType.STRING) {
56+
return errors;
57+
}
58+
if (!isLegalDateTime(node.textValue())) {
59+
errors.add(buildValidationMessage(at, node.textValue(), formatName));
60+
}
61+
return Collections.unmodifiableSet(errors);
62+
}
63+
64+
private boolean isLegalDateTime(String string) {
65+
Matcher matcher = RFC3339_PATTERN.matcher(string);
66+
StringBuilder pattern = new StringBuilder();
67+
StringBuilder dateTime = new StringBuilder();
68+
// Validate the format
69+
if (!matcher.matches()) {
70+
logger.error("Failed to apply RFC3339 pattern on " + string);
71+
return false;
72+
}
73+
// Validate the date/time content
74+
String year = matcher.group(1);
75+
String month = matcher.group(2);
76+
String day = matcher.group(3);
77+
dateTime.append(year).append('-').append(month).append('-').append(day);
78+
pattern.append("yyyy-MM-dd");
79+
80+
boolean isTimeGiven = matcher.group(4) != null;
81+
String timeZoneShiftRegexGroup = matcher.group(9);
82+
boolean isTimeZoneShiftGiven = timeZoneShiftRegexGroup != null;
83+
String hour = null;
84+
String minute = null;
85+
String second = null;
86+
String milliseconds = null;
87+
String timeShiftHour = null;
88+
String timeShiftMinute = null;
89+
90+
if (!isTimeGiven && DATETIME.equals(formatName) || (isTimeGiven && DATE.equals(formatName))) {
91+
logger.error("The supplied date/time format type does not match the specification, expected: " + formatName);
92+
return false;
93+
}
94+
95+
if (!isTimeGiven && isTimeZoneShiftGiven) {
96+
logger.error("Invalid date/time format, cannot specify time zone shift" +
97+
" without specifying time: " + string);
98+
return false;
99+
}
100+
101+
if (isTimeGiven) {
102+
hour = matcher.group(5);
103+
minute = matcher.group(6);
104+
second = matcher.group(7);
105+
dateTime.append('T').append(hour).append(':').append(minute).append(':').append(second);
106+
pattern.append("'T'hh:mm:ss");
107+
if (matcher.group(8) != null) {
108+
// Normalize milliseconds to 3-length digit
109+
milliseconds = matcher.group(8);
110+
if (milliseconds.length() > 4) {
111+
milliseconds = milliseconds.substring(0, 4);
112+
} else {
113+
while (milliseconds.length() < 4) {
114+
milliseconds += "0";
115+
}
116+
}
117+
dateTime.append(milliseconds);
118+
pattern.append(".SSS");
119+
}
120+
}
121+
122+
if (isTimeGiven && isTimeZoneShiftGiven) {
123+
if (Character.toUpperCase(timeZoneShiftRegexGroup.charAt(0)) == 'Z') {
124+
dateTime.append('Z');
125+
pattern.append("'Z'");
126+
} else {
127+
timeShiftHour = matcher.group(11);
128+
timeShiftMinute = matcher.group(12);
129+
dateTime.append(matcher.group(10).charAt(0)).append(timeShiftHour).append(':').append(timeShiftMinute);
130+
pattern.append("XXX");
131+
}
132+
}
133+
return validateDateTime(dateTime.toString(), pattern.toString());
134+
}
135+
136+
private boolean validateDateTime(String dateTime, String pattern) {
137+
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
138+
sdf.setLenient(false);
139+
return sdf.parse(dateTime, new ParsePosition(0)) != null;
140+
}
141+
}

src/main/java/com/networknt/schema/FormatKeyword.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ public class FormatKeyword implements Keyword {
2626
private final ValidatorTypeCode type;
2727
private final Map<String, Format> formats;
2828

29+
private final String DATE = "date";
30+
private final String DATE_TIME = "date-time";
31+
2932
public FormatKeyword(ValidatorTypeCode type, Map<String, Format> formats) {
3033
this.type = type;
3134
this.formats = formats;
@@ -34,23 +37,24 @@ public FormatKeyword(ValidatorTypeCode type, Map<String, Format> formats) {
3437
Collection<Format> getFormats() {
3538
return Collections.unmodifiableCollection(formats.values());
3639
}
37-
40+
3841
@Override
3942
public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext)
4043
throws Exception {
4144
Format format = null;
4245
if (schemaNode != null && schemaNode.isTextual()) {
4346
String formatName = schemaNode.textValue();
4447
format = formats.get(formatName);
48+
// Validate date and time separately
49+
if (formatName.equals(DATE) || formatName.equals(DATE_TIME)) {
50+
return new DateTimeValidator(schemaPath, schemaNode, parentSchema, validationContext, formatName);
51+
}
4552
}
46-
4753
return new FormatValidator(schemaPath, schemaNode, parentSchema, validationContext, format);
4854
}
49-
55+
5056
@Override
5157
public String getValue() {
5258
return type.getValue();
5359
}
54-
55-
5660
}

0 commit comments

Comments
 (0)