Custom Validators – Cron Job Expression Validator
Ever had to validate Cron Job Expression validator in Flex? No? Well, that really isn’t surprising. Recently I have had a client that needed the a Flex app to do lots of Cron Job methods, so one of the first things I had to do was create a Cron Job Expression validator to make sure that the entered in Cron Job Expression was valid before continuing with the application.
I COULD have built a function that checks the user’s entered text, but instead I felt it was important to stick with components provided by the Flex framework, in this case, validators.
Now rather than having to learn a new function, you can use the CronJobExpressionValidator just like you used a StringValidator or any other validator.
Let’s look at how to create a custom validator and then look at the code in the CronJobExpressionValidator.
We’re going to start everything with a class (as always), so let’s determine the superclass that your validator will inherit from. At the very least your validator MUST extend the Validator class, for our example I extended the StringValidator class so that I have access to max and min length validations along with the validation rules I will add.
This will create:
1 2 3 4 5 6 7 8 9 10 11 12 | package com.unitedmindset.validators { import mx.validators.StringValidator; public class CronExpressionValidator extends StringValidator { public function CronExpressionValidator() { super(); } } } |
Next we MUST override the doValidation method. This function is meant to be the starting point for your validator.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package com.unitedmindset.validators { import mx.validators.StringValidator; public class CronExpressionValidator extends StringValidator { public function CronExpressionValidator() { super(); } override protected function doValidation(value:Object) : Array { super.doValidation(value); } } } |
The next method we will use is the validateCronExpression private method. This is the function that holds the meat of your class and the logic that will actually determine if the entered value is valid. The entire function (and actually the entire class is shown below). As this last function is dependent on what you are planning to validate, I didn’t worry about breaking down the individual steps.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 | package com.unitedmindset.validators { import mx.events.ValidationResultEvent; import mx.validators.NumberValidator; import mx.validators.RegExpValidator; import mx.validators.StringValidator; import mx.validators.ValidationResult; public class CronExpressionValidator extends StringValidator { public function CronExpressionValidator() { super(); } /** * @private * Loads resources for this class * */ private static function validateCronExpression(validator:CronExpressionValidator, value:String, baseField:String = null):Array { var results:Array = new Array(); if(!value) { results.push(new ValidationResult(true, baseField, "required", "This field is required.")); return results; } //split and test length var expressionArray:Array = value.split(" "); var len:int = expressionArray.length; if((len != 6) && (len != 7)) { results.push(new ValidationResult(true, baseField, "wrongFormat", validator.wrongFormatError)); return results; } // check only one question mark var match:Array = value.match(/\?/g); if(match && match.length>1){ results.push(new ValidationResult(true, baseField, "wrongFormat", validator.wrongFormatError)); return results; } // check only one question mark var dayOfTheMonthWildcard:String = null; //if appropriate length test parts // [0] Seconds 0-59 , - * / if(CronExpressionValidator.isNotWildCard(expressionArray[0], /[\*]/gi)) results = CronExpressionValidator.segmentValidator("([0-9,\\-\\/])", expressionArray[0], results, [0, 59], "seconds"); // [1] Minutes 0-59 , - * / if(CronExpressionValidator.isNotWildCard(expressionArray[1], /[\*]/gi)) results = CronExpressionValidator.segmentValidator("([0-9,\\-\\/])", expressionArray[1], results, [0, 59], "minutes"); // [2] Hours 0-23 , - * / if(CronExpressionValidator.isNotWildCard(expressionArray[2], /[\*]/gi)) results = CronExpressionValidator.segmentValidator("([0-9,\\-\\/])", expressionArray[2], results, [0, 23], "hours"); // [3] Day of month 1-31 , - * ? / L W C if(CronExpressionValidator.isNotWildCard(expressionArray[3], /[\*\?]/gi)){ results = CronExpressionValidator.segmentValidator("([0-9LWC,\\-\\/])", expressionArray[3], results, [1, 31], "days of the month"); } else { dayOfTheMonthWildcard = expressionArray[3] as String; } // [4] Month 1-12 or JAN-DEC , - * / if(CronExpressionValidator.isNotWildCard(expressionArray[4], /[\*]/gi)) { expressionArray[4] = CronExpressionValidator.convertMonthsToInteger(expressionArray[4]); results = CronExpressionValidator.segmentValidator("([0-9,\\-\\/])", expressionArray[4], results, [1, 12], "months"); } // [5] Day of week 1-7 or SUN-SAT , - * ? / L C # if(CronExpressionValidator.isNotWildCard(expressionArray[5], /[\*\?]/gi)) { expressionArray[5] = CronExpressionValidator.convertDaysToInteger(expressionArray[5]); results = CronExpressionValidator.segmentValidator("([0-9LC,#\\-\\/])", expressionArray[5], results, [1, 7], "days of the week"); } else { if(dayOfTheMonthWildcard == String(expressionArray[5])){ results.push(new ValidationResult(true, baseField, "wrongFormat", validator.wrongFormatError)); return results; } } // [6] Year empty or 1970-2099 , - * / if(len == 7) { if(CronExpressionValidator.isNotWildCard(expressionArray[6], /[\*]/gi)) results = CronExpressionValidator.segmentValidator("([0-9,\\-\\/])", expressionArray[6], results, [1970, 2099], "years"); } return results; } override protected function doValidation(value:Object):Array { var results:Array = super.doValidation(value); //return if there are errors //or if the required property is set to false and length is 0 var val:String = value ? String(value) : ""; if(results.length > 0 || ((val.length == 0) && !required)) return results; else return CronExpressionValidator.validateCronExpression(this, (value as String), null); } //---------------------------------- // convertMonths //---------------------------------- private static function convertMonthsToInteger(value:Object):String { var v:String = value as String; v = v.replace(/JAN/gi, "1"); v = v.replace(/FEB/gi, "2"); v = v.replace(/MAR/gi, "3"); v = v.replace(/APR/gi, "4"); v = v.replace(/MAY/gi, "5"); v = v.replace(/JUN/gi, "6"); v = v.replace(/JUL/gi, "7"); v = v.replace(/AUG/gi, "8"); v = v.replace(/SEP/gi, "9"); v = v.replace(/OCT/gi, "10"); v = v.replace(/NOV/gi, "11"); v = v.replace(/DEC/gi, "12"); return v; } //---------------------------------- // convertDays //---------------------------------- private static function convertDaysToInteger(value:Object):String { var v:String = value as String; v = v.replace(/SUN/gi, "1"); v = v.replace(/MON/gi, "2"); v = v.replace(/TUE/gi, "3"); v = v.replace(/WED/gi, "4"); v = v.replace(/THU/gi, "5"); v = v.replace(/FRI/gi, "6"); v = v.replace(/SAT/gi, "7"); return v; } //---------------------------------- // isNotWildcard //---------------------------------- private static function isNotWildCard(value:Object, expression:RegExp):Boolean { var match:Array = String(value).match(expression); return(match.length == 0 || match == null) ? true : false; } //---------------------------------- // segmentValidator //---------------------------------- private static function segmentValidator(expression:String, value:Object, results:Array, range:Array, segmentName:String):Array { var v:String = value as String; var numbers:Array = new Array(); // first, check for any improper segments var validator:RegExpValidator = new RegExpValidator(); validator.required = true; validator.expression = expression; validator.noMatchError = "The " + segmentName + " segment is invalid."; validator.flags = "gi" var resultEvent:ValidationResultEvent = validator.validate(v); // if(resultEvent.type == ValidationResultEvent.INVALID){ return results.concat(resultEvent.results); } else { if(resultEvent.results && resultEvent.results.length != v.length){ results.push(new ValidationResult(true, null, "noMatch", "The " + segmentName + " segment is invalid.")); return results; } } //check duplicate types // check only one L var dupMatch:Array = value.match(/L/gi); if(dupMatch.length>1){ results.push(new ValidationResult(true, null, "noMatch", "The " + segmentName + " segment is invalid.")); return results; } //look through the segments //break up segments on ',' //check for special cases L,W,C,/,#,- var split:Array = v.split(","); var i:int = -1; var l:int = split.length; var match:Array; while(++i < l) { // set vars var checkSegment:String = spliti] as String; var n:Number; var pattern:RegExp = /(\w*)/; match = pattern.exec(checkSegment); //if just number pattern = /(\w*)\-?\d+(\w*)/; match = pattern.exec(checkSegment); if(match && match[0] == checkSegment && checkSegment.indexOf("L")==-1 && checkSegment.indexOf("l")==-1 && checkSegment.indexOf("C")==-1 && checkSegment.indexOf("c")==-1 && checkSegment.indexOf("W")==-1 && checkSegment.indexOf("w")==-1 && checkSegment.indexOf("/")==-1 && (checkSegment.indexOf("-")==-1 || checkSegment.indexOf("-")==0) && checkSegment.indexOf("#")==-1) { n = match[0]; if(n && !(isNaN(n))) numbers.push(n); else if(match[0] == "0") numbers.push(n); continue; } // includes L, C, or w pattern = /(\w*)L|C|W(\w*)/i; match = pattern.exec(checkSegment); if(match && match[0] != "" && ( checkSegment.indexOf("L")>-1 || checkSegment.indexOf("l")>-1 || checkSegment.indexOf("C")>-1 || checkSegment.indexOf("c")>-1 || checkSegment.indexOf("W")>-1 || checkSegment.indexOf("w")>-1 )) { //check just l or L if(checkSegment == "L" || checkSegment == "l") continue; pattern = /(\w*)\d+(l|c|w)?(\w*)/i; match = pattern.exec(checkSegment); //if something before or after if(!match || match[0] != checkSegment) { results.push(new ValidationResult(true, null, "noMatch", "The " + segmentName + " segment is invalid.")); continue; } //get the number var numCheck:String = match0] as String; numCheck = numCheck.replace(/(l|c|w)/ig,""); n = Number(numCheck); if(n && !(isNaN(n))) numbers.push(n); else if(match[0] == "0") numbers.push(n); continue; } var numberSplit:Array; //includes / if(checkSegment.indexOf("/") > -1) { // take first # numberSplit = checkSegment.split("/"); if(numberSplit.length != 2) { results.push(new ValidationResult(true, null, "noMatch", "The " + segmentName + " segment is invalid.")); continue; } else { n = numberSplit[0]; if(n && !(isNaN(n))) numbers.push(n); else if(numberSplit[0] == "0") numbers.push(n); continue; } } //includes # if(checkSegment.indexOf("#") > -1) { // take first # numberSplit = checkSegment.split("#"); if(numberSplit.length != 2) { results.push(new ValidationResult(true, null, "noMatch", "The " + segmentName + " segment is invalid.")); continue; } else { n = numberSplit[0]; if(n && !(isNaN(n))) numbers.push(n); else if(numberSplit[0] == "0") numbers.push(n); continue; } } //includes - if(checkSegment.indexOf("-") > 0) { // take both # numberSplit = checkSegment.split("-"); if(numberSplit.length != 2) { results.push(new ValidationResult(true, null, "noMatch", "The " + segmentName + " segment is invalid.")); continue; } else if(Number(numberSplit[0])>Number(numberSplit[1])){ results.push(new ValidationResult(true, null, "noMatch", "The " + segmentName + " segment is invalid.")); continue; } else { n = numberSplit[0]; if(n && !(isNaN(n))) numbers.push(n); else if(numberSplit[0] == "0") numbers.push(n); n = numberSplit[1]; if(n && !(isNaN(n))) numbers.push(n); else if(numberSplit[1] == "0") numbers.push(n); continue; } } } // lastly, check that all the found numbers are in range i = -1; l = numbers.length; if(l == 0) return results; var validatorN:NumberValidator = new NumberValidator(); validatorN.required = true; validatorN.minValue = range0] as Number; validatorN.maxValue = range1] as Number; validatorN.lowerThanMinError = "The number for the " + segmentName + " segment entered can't be lower than " + range[0] + "."; validatorN.exceedsMaxError = "The number entered for the " + segmentName + " segment can't exceed " + range[1] + "."; while(++i < l) { var vResultEvent:ValidationResultEvent = validatorN.validate(numbers[i]); if(vResultEvent.type == ValidationResultEvent.INVALID && vResultEvent.results && vResultEvent.results.length > 0) results = results.concat(vResultEvent.results); } return results; } //---------------------------------- // wrongFormatError //---------------------------------- /** * @private * Storage for the wrongFormatError property. */ private var _wrongFormatError:String = "The Cron Expression must be in the form 'E E E E E E' or 'E E E E E E E'. Without spaces within the segments."; Inspectable"Errors",defaultValue="null")] /** * Error message when the value is incorrectly formatted. * * @default "The Cron Expression must be in the form 'E E E E E E' or 'E E E E E E E'. Without spaces within the segments." */ public function get wrongFormatError():String { return _wrongFormatError; } /** * @private */ public function set wrongFormatError(value:String):void { _wrongFormatError = value != null ? value : "The Cron Expression must be in the form 'E E E E E E' or 'E E E E E E E'. Without spaces within the segments."; } } } |
Below is my cron job expression test.






Hi,
I’m looking all over the net for dealing with the following cron expression:
0 0 0 0W * ?
it doesn’t seem valid to me (0W as day-of-the-month), yet Quartz CronExpression library and your validator validate the expression, only Quartz’s computeFirstFireTime() gets stucks in an infinite loop.
Can you please confirm that this expression should cause a parse exception?
Thanks ahead.
For the day-of-the-month segment the valid values are between 1 and 31 and then “, – * ? / L W C”, so 0 shouldn’t be valid. You can review these specifications at: http://quartz.sourceforge.net/javadoc/org/quartz/CronTrigger.html. HTH
Good work.
This validator was helpful to me