Skip to content

Commit 7114855

Browse files
committed
Enhance the DateTimeValidation to support validate time zone shift and millisecond
1 parent fcf0bf8 commit 7114855

File tree

4 files changed

+104
-40
lines changed

4 files changed

+104
-40
lines changed

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

Lines changed: 75 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,22 @@
2525
import java.util.Collections;
2626
import java.util.LinkedHashSet;
2727
import java.util.Set;
28-
import java.util.regex.PatternSyntaxException;
28+
import java.util.regex.Matcher;
29+
import java.util.regex.Pattern;
2930

3031
public class DateTimeValidator extends BaseJsonValidator implements JsonValidator {
3132
private static final Logger logger = LoggerFactory.getLogger(DateTimeValidator.class);
3233

3334
private final String DATE = "date";
3435
private final String DATE_TIME = "date-time";
3536

36-
private final String formatName;
37-
private final Format format;
37+
private static final Pattern RFC3339_PATTERN = Pattern.compile(
38+
"^(\\d{4})-(\\d{2})-(\\d{2})" // yyyy-MM-dd
39+
+ "([Tt](\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?)?" // 'T'HH:mm:ss.milliseconds
40+
+ "([Zz]|([+-])(\\d{2}):(\\d{2}))?");
3841

39-
public DateTimeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, String formatName, Format format) {
42+
public DateTimeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
4043
super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.DATETIME, validationContext);
41-
this.formatName = formatName;
42-
this.format = format;
4344
parseErrorCode(getValidatorType().getErrorCodeKey());
4445
}
4546

@@ -52,43 +53,81 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
5253
if (nodeType != JsonType.STRING) {
5354
return errors;
5455
}
55-
56-
if (formatName != null) {
57-
if (formatName.equals(DATE) && !isLegalDate(node.textValue()) || (formatName.equals(DATE_TIME) && !isLegalDateTime(node.textValue()))) {
58-
errors.add(buildValidationMessage(at, node.textValue(), formatName));
59-
}
56+
if (!isLegalDateTime(node.textValue())) {
57+
errors.add(buildValidationMessage(at, node.textValue()));
6058
}
61-
6259
return Collections.unmodifiableSet(errors);
6360
}
6461

65-
private boolean isLegalDate(String string) {
66-
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
67-
sdf.setLenient(false);
68-
return sdf.parse(string, new ParsePosition(0)) != null;
69-
}
62+
private boolean isLegalDateTime(String string) {
63+
Matcher matcher = RFC3339_PATTERN.matcher(string);
64+
StringBuilder pattern = new StringBuilder();
65+
StringBuilder dateTime = new StringBuilder();
66+
// Validate the format
67+
if (!matcher.matches()) {
68+
logger.error("Failed to apply RFC3339 pattern on " + string);
69+
return false;
70+
}
71+
// Validate the date/time content
72+
String year = matcher.group(1);
73+
String month = matcher.group(2);
74+
String day = matcher.group(3);
75+
dateTime.append(year).append('-').append(month).append('-').append(day);
76+
pattern.append("yyyy-MM-dd");
7077

71-
private boolean isLegalTime(String string) {
72-
String time = string.split("\\.")[0];
73-
SimpleDateFormat sdf = new SimpleDateFormat("hh:mm:ss");
74-
sdf.setLenient(false);
75-
return sdf.parse(time, new ParsePosition(0)) != null;
76-
}
78+
boolean isTimeGiven = matcher.group(4) != null;
79+
String timeZoneShiftRegexGroup = matcher.group(9);
80+
boolean isTimeZoneShiftGiven = timeZoneShiftRegexGroup != null;
81+
String hour = null;
82+
String minute = null;
83+
String second = null;
84+
String milliseconds = null;
85+
String timeShiftHour = null;
86+
String timeShiftMinute = null;
7787

78-
private boolean isLegalDateTime(String string) {
79-
// Check the format
80-
try {
81-
if (!format.matches(string)) {
82-
return false;
88+
if (!isTimeGiven && isTimeZoneShiftGiven) {
89+
throw new NumberFormatException("Invalid date/time format, cannot specify time zone shift" +
90+
" without specifying time: " + string);
91+
}
92+
93+
if (isTimeGiven) {
94+
hour = matcher.group(5);
95+
minute = matcher.group(6);
96+
second = matcher.group(7);
97+
dateTime.append('T').append(hour).append(':').append(minute).append(':').append(second);
98+
pattern.append("'T'hh:mm:ss");
99+
if (matcher.group(8) != null) {
100+
// Normalize milliseconds to 3-length digit
101+
milliseconds = matcher.group(8);
102+
if (milliseconds.length() > 4) {
103+
milliseconds = milliseconds.substring(0, 4);
104+
} else {
105+
while (milliseconds.length() < 4) {
106+
milliseconds += "0";
107+
}
108+
}
109+
dateTime.append(milliseconds);
110+
pattern.append(".SSS");
83111
}
84-
} catch (PatternSyntaxException pse) {
85-
// String is considered valid if pattern is invalid
86-
logger.error("Failed to apply pattern: Invalid RE syntax [" + format.getName() + "]", pse);
87112
}
88-
// Check the contents
89-
String[] dateTime = string.split("\\s|T|t", 2);
90-
String date = dateTime[0];
91-
String time = dateTime[1];
92-
return isLegalDate(date) && isLegalTime(time);
113+
114+
if (isTimeGiven && isTimeZoneShiftGiven) {
115+
if (Character.toUpperCase(timeZoneShiftRegexGroup.charAt(0)) == 'Z') {
116+
dateTime.append('Z');
117+
pattern.append("'Z'");
118+
} else {
119+
timeShiftHour = matcher.group(11);
120+
timeShiftMinute = matcher.group(12);
121+
dateTime.append(matcher.group(10).charAt(0)).append(timeShiftHour).append(':').append(timeShiftMinute);
122+
pattern.append("XXX");
123+
}
124+
}
125+
return validateDateTime(dateTime.toString(), pattern.toString());
126+
}
127+
128+
private boolean validateDateTime(String dateTime, String pattern) {
129+
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
130+
sdf.setLenient(false);
131+
return sdf.parse(dateTime, new ParsePosition(0)) != null;
93132
}
94133
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSc
4747
format = formats.get(formatName);
4848
// Validate date and time separately
4949
if (formatName.equals(DATE) || formatName.equals(DATE_TIME)) {
50-
return new DateTimeValidator(schemaPath, schemaNode, parentSchema, validationContext, formatName, format);
50+
return new DateTimeValidator(schemaPath, schemaNode, parentSchema, validationContext);
5151
}
5252
}
5353
return new FormatValidator(schemaPath, schemaNode, parentSchema, validationContext, format);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSc
6363
TYPE("type", "1029", new MessageFormat("{0}: {1} found, {2} expected")),
6464
UNION_TYPE("unionType", "1030", new MessageFormat("{0}: {1} found, but {2} is required")),
6565
UNIQUE_ITEMS("uniqueItems", "1031", new MessageFormat("{0}: the items in the array must be unique")),
66-
DATETIME("date-time", "1034", new MessageFormat("{0}: {1} is not a legal {2}"));
66+
DATETIME("date-time", "1034", new MessageFormat("{0}: {1} is an invalid date/time"));
6767

6868
private static Map<String, ValidatorTypeCode> constants = new HashMap<String, ValidatorTypeCode>();
6969

src/test/resources/tests/optional/format.json

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,38 @@
6565
},
6666
{
6767
"description": "an invalid date-time string",
68-
"data": "2019-02-29T08:60:06Z",
68+
"data": "2019-02-28T08:60:06Z",
6969
"valid": false
7070
},
7171
{
7272
"description": "an invalid date-time string",
73-
"data": "2019-02-29T08:30:60Z",
73+
"data": "2019-02-28T08:30:60Z",
7474
"valid": false
75+
},
76+
{
77+
"description": "a valid date-time string",
78+
"data": "2019-02-28T08:30:59.12345+08:00",
79+
"valid": true
80+
},
81+
{
82+
"description": "a valid date-time string",
83+
"data": "2019-02-28T08:30:59.12345-12:00",
84+
"valid": true
85+
},
86+
{
87+
"description": "an invalid date-time string",
88+
"data": "2019-02-28T08:30:59.12345-60:00",
89+
"valid": false
90+
},
91+
{
92+
"description": "an invalid date-time string",
93+
"data": "2019-02-28T08:30:59.12345-12:60",
94+
"valid": false
95+
},
96+
{
97+
"description": "a valid date-time string",
98+
"data": "2019-02-28T08:30:59.99999z",
99+
"valid": true
75100
}
76101
]
77102
},

0 commit comments

Comments
 (0)