25
25
import java .util .Collections ;
26
26
import java .util .LinkedHashSet ;
27
27
import java .util .Set ;
28
- import java .util .regex .PatternSyntaxException ;
28
+ import java .util .regex .Matcher ;
29
+ import java .util .regex .Pattern ;
29
30
30
31
public class DateTimeValidator extends BaseJsonValidator implements JsonValidator {
31
32
private static final Logger logger = LoggerFactory .getLogger (DateTimeValidator .class );
32
33
33
34
private final String DATE = "date" ;
34
35
private final String DATE_TIME = "date-time" ;
35
36
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}))?" );
38
41
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 ) {
40
43
super (schemaPath , schemaNode , parentSchema , ValidatorTypeCode .DATETIME , validationContext );
41
- this .formatName = formatName ;
42
- this .format = format ;
43
44
parseErrorCode (getValidatorType ().getErrorCodeKey ());
44
45
}
45
46
@@ -52,43 +53,81 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
52
53
if (nodeType != JsonType .STRING ) {
53
54
return errors ;
54
55
}
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 ()));
60
58
}
61
-
62
59
return Collections .unmodifiableSet (errors );
63
60
}
64
61
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" );
70
77
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 ;
77
87
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" );
83
111
}
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 );
87
112
}
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 ;
93
132
}
94
133
}
0 commit comments