77import dev .learning .xapi .model .validation .constraints .ValidDuration ;
88import jakarta .validation .ConstraintValidator ;
99import jakarta .validation .ConstraintValidatorContext ;
10+ import java .util .regex .Matcher ;
11+ import java .util .regex .Pattern ;
1012
1113/**
1214 * Validates ISO 8601:2004 duration format strings.
1719 */
1820public class DurationValidator implements ConstraintValidator <ValidDuration , String > {
1921
22+ // Simple patterns - each validates a single component type
23+ private static final Pattern WEEK = Pattern .compile ("^\\ d+W$" , Pattern .CASE_INSENSITIVE );
24+ private static final Pattern DATE =
25+ Pattern .compile ("^(\\ d+Y)?(\\ d+M)?(\\ d+D)?$" , Pattern .CASE_INSENSITIVE );
26+ private static final Pattern TIME =
27+ Pattern .compile ("^(\\ d+H)?(\\ d+M)?((\\ d+\\ .\\ d+|\\ d+)S)?$" , Pattern .CASE_INSENSITIVE );
28+
2029 @ Override
2130 public boolean isValid (String value , ConstraintValidatorContext context ) {
2231 if (value == null ) {
2332 return true ;
2433 }
2534
26- String upper = value .toUpperCase ();
27-
28- // Must start with P
29- if (!upper .startsWith ("P" ) || upper .length () < 2 ) {
35+ // Must start with P and have at least one component
36+ if (!value .toUpperCase ().startsWith ("P" ) || value .length () < 2 ) {
3037 return false ;
3138 }
3239
33- String rest = upper .substring (1 );
40+ String rest = value .substring (1 );
3441
3542 // Week format: P[n]W
36- if (rest . endsWith ( "W" ) && rest . length () > 1 ) {
37- return isDigits ( rest . substring ( 0 , rest . length () - 1 )) ;
43+ if (WEEK . matcher ( rest ). matches () ) {
44+ return true ;
3845 }
3946
4047 // Split by T to get date and time parts
41- int tpos = rest .indexOf ('T' );
48+ int tpos = rest .toUpperCase (). indexOf ('T' );
4249 String datePart = tpos >= 0 ? rest .substring (0 , tpos ) : rest ;
4350 String timePart = tpos >= 0 ? rest .substring (tpos + 1 ) : "" ;
4451
@@ -47,65 +54,22 @@ public boolean isValid(String value, ConstraintValidatorContext context) {
4754 return false ;
4855 }
4956
50- // Validate date part (Y, M, D in order)
51- if (!datePart .isEmpty () && !isValidPart (datePart , "YMD" , false )) {
52- return false ;
53- }
54-
55- // Validate time part (H, M, S in order, S can be decimal)
56- if (!timePart .isEmpty () && !isValidPart (timePart , "HMS" , true )) {
57- return false ;
58- }
59-
60- return true ;
61- }
62-
63- private boolean isValidPart (String part , String designators , boolean allowDecimalLast ) {
64- int pos = 0 ;
65- boolean found = false ;
66-
67- for (int i = 0 ; i < designators .length (); i ++) {
68- char designator = designators .charAt (i );
69- int idx = part .indexOf (designator , pos );
70- if (idx > pos ) {
71- String digits = part .substring (pos , idx );
72- boolean isLast = i == designators .length () - 1 ;
73- if (!(isLast && allowDecimalLast ? isDigitsOrDecimal (digits ) : isDigits (digits ))) {
74- return false ;
75- }
76- pos = idx + 1 ;
77- found = true ;
78- } else if (idx == pos ) {
79- return false ; // Designator without preceding digits
57+ // Validate date part - must match pattern and have at least one component
58+ if (!datePart .isEmpty ()) {
59+ Matcher m = DATE .matcher (datePart );
60+ if (!m .matches () || (m .group (1 ) == null && m .group (2 ) == null && m .group (3 ) == null )) {
61+ return false ;
8062 }
8163 }
8264
83- return found && pos == part .length ();
84- }
85-
86- private boolean isDigits (String s ) {
87- if (s .isEmpty ()) {
88- return false ;
89- }
90- for (int i = 0 ; i < s .length (); i ++) {
91- if (!Character .isDigit (s .charAt (i ))) {
65+ // Validate time part - must match pattern and have at least one component
66+ if (!timePart .isEmpty ()) {
67+ Matcher m = TIME .matcher (timePart );
68+ if (!m .matches () || (m .group (1 ) == null && m .group (2 ) == null && m .group (3 ) == null )) {
9269 return false ;
9370 }
9471 }
95- return true ;
96- }
9772
98- private boolean isDigitsOrDecimal (String s ) {
99- if (s .isEmpty ()) {
100- return false ;
101- }
102- int dotPos = s .indexOf ('.' );
103- if (dotPos < 0 ) {
104- return isDigits (s );
105- }
106- return dotPos > 0
107- && dotPos < s .length () - 1
108- && isDigits (s .substring (0 , dotPos ))
109- && isDigits (s .substring (dotPos + 1 ));
73+ return true ;
11074 }
11175}
0 commit comments