001package com.randomnoun.common; 002 003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a 004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html) 005 */ 006 007import java.text.ParseException; 008import java.util.*; 009 010import jakarta.servlet.http.HttpServletRequest; 011 012/** 013 * The ErrorList class is a a fairly generic class for containing validation errors for 014 * input forms, similar to the struts ActionErrors class. Each error within this class can 015 * contain a 'short' and 'long' description, where the short description is normally a 016 * categorisation of the error and the longer description describes what has happened and 017 * how to fix the problem; (e.g. shortText="Missing field", 018 * longText="The field 'id' is mandatory. Please enter a value for this field."). The short 019 * description is normally rendered by <code>errorHeader.jsp</code> in bold before the long 020 * description. An individual error also may contain a list of fields that it applies to 021 * (e.g. a 'mandatory inclusive' error may apply to several fields at once), and a 022 * severity (normally set to {@link #SEVERITY_INVALID}. Errors are displayed by the 023 * <code>errorHeader.jsp</code> JSP, which is typically included at the top of any page 024 * that contains an input form. An error without a severity supplied is assumed to be 025 * of SEVERITY_INVALID, and an error without any fields supplied is assumed to be 026 * associated with the entire form, rather than a specific set of fields. 027 * 028 * <p>Errors are inserted into an ErrorList by using one of the addError methods: 029 * <ul> 030 * <li> {@link #addError(String, String)} - add an invalid form error 031 * <li> {@link #addError(String, String, int)} - add an form error with a specific severity 032 * <li> {@link #addError(String, String, String)} - add an invalid field error 033 * <li> {@link #addError(String, String, String, int)} - add an invalid field error with a specific severity 034 * </ul> 035 * 036 * <p>Code that uses an ErrorList to perform validation may attach an object to the 037 * errorList instance to perform standard validations and to localise error messages. 038 * 039 * @author knoxg 040 */ 041public class ErrorList extends ArrayList<ErrorList.ErrorData> 042 implements java.io.Serializable 043{ 044 045 /** Generated serialVersionUID */ 046 private static final long serialVersionUID = 9113362689694606840L; 047 048 /** Severity level indicating 'not an error' (e.g. informational only). Used to indicate successful operations */ 049 public static final int SEVERITY_OK = 0; // Not an error 050 051 /** invalid user-supplied data; end-user can fix situation */ 052 public static final int SEVERITY_INVALID = 1; 053 054 /** invalid system-supplied data; end-user cannot fix situation */ 055 public static final int SEVERITY_ERROR = 2; 056 057 /** internal error (e.g. EJB/LDAP connection failure) */ 058 public static final int SEVERITY_FATAL = 3; 059 060 /** unrecoverable internal error (can't think of anything here, but the world ending could be one */ 061 public static final int SEVERITY_PANIC = 4; 062 063 // these were created after the 5 above, which is why they have higher ID numbers 064 // at a later stage will renumber these so that 065 // OK < INFO < WARNING < INVALID < ERROR < FATAL < PANIC 066 067 /** information message; can be used in addition to SEVERITY_OK for additional text */ 068 public static final int SEVERITY_INFO = 5; 069 070 /** possibly incorrect user-supplied data; operation still succeeds but may return incorrect results */ 071 public static final int SEVERITY_WARNING = 6; 072 073 074 // the thing we're validating 075 private transient Object attachedObject; 076 077 // contains error messages 078 private transient ResourceBundle attachedBundle; 079 private transient String bundleFormat; 080 private transient String attachedFieldFormat; 081 private transient Locale locale; 082 083 /** ErrorInfo inner class - contains information related to 084 * a single error 085 */ 086 public static class ErrorData 087 extends HashMap<String, Object> 088 { 089 /** Generated serialVersionUID */ 090 private static final long serialVersionUID = -176737615399095851L; 091 092 /** 093 * Creates a new ErrorInfo object. 094 * 095 * @param shortText a short, descriptive string for this error 096 * @param longText a more lengthy description of this error 097 * @param errorField comma-separated list of field names that caused 098 * this error 099 * @param severity the severity of this error 100 */ 101 public ErrorData(String shortText, String longText, String errorField, 102 int severity) 103 { 104 super(); 105 put("shortText", shortText); 106 put("longText", longText); 107 put("field", errorField); 108 put("severity", Integer.valueOf(severity)); 109 } 110 111 /** Retrieves the type for this error 112 * @return the type for this error 113 */ 114 public String getShortText() 115 { 116 return (String)get("shortText"); 117 } 118 119 /** Retrieves the description for this error 120 * @return the description for this error 121 */ 122 public String getLongText() 123 { 124 return (String)get("longText"); 125 } 126 127 /** Retrieves the description for this error, with newlines converted to <br/>s. 128 * When displaying with <c:out>, set the escapeXml attribute to false 129 * 130 * @return the description for this error 131 */ 132 public String getLongTextWithNewlines() 133 { 134 String longText = (String) get("longText"); 135 longText=Text.escapeHtml(longText); 136 longText=Text.replaceString(longText, "\n", "<br/>"); 137 return longText; 138 } 139 140 141 /** Retrieves a comma-separated list of fields that caused this error 142 * @return a comma-separated list of fields that caused this error 143 */ 144 public String getField() 145 { 146 return (String)get("field"); 147 } 148 149 /** Retrieves the severity of this error 150 * @return the severity of this error 151 */ 152 public int getSeverity() 153 { 154 return ((Integer)get("severity")).intValue(); 155 } 156 157 /** Retrieves a string representation of this error 158 * @return a string representation of this error 159 */ 160 public String toString() 161 { 162 return "{shortText='" + getShortText() + "', longText='" + getLongText() + 163 "', fields='" + getField() + "', severity=" + getSeverity() + "}"; 164 } 165 } 166 167 /** 168 * Create a new, empty ErrorData object. 169 */ 170 public ErrorList() 171 { 172 super(); 173 } 174 175 /** 176 * Adds an error. This is the "real" addError() method; all others delegate to this 177 * one. 178 * 179 * @param errorField a comma-separated list of field names that caused this error 180 * @param shortText a short string conveying the error type (e.g. 'Missing field') 181 * @param longText a longer string describing the nature of the error and how to resolve it 182 * (e.g. 'The field 'id' is mandatory. Please enter a value for this field.') 183 * @param severity the severity of this error (one of the SEVERITY_* constants of this class). 184 * 185 * @see SEVERITY_INVALID 186 */ 187 public void addError(String errorField, String shortText, String longText, 188 int severity) 189 { 190 // if we have an attachedFieldFormat, then format each field to this format 191 if (attachedFieldFormat != null && errorField != null) { 192 try { 193 List<String> fields = Text.parseCsv(errorField); 194 errorField = ""; 195 for (Iterator<String> i = fields.iterator(); i.hasNext();) { 196 String field = i.next(); 197 errorField = errorField + getFieldName(field) + 198 (i.hasNext() ? "," : ""); 199 } 200 } catch (ParseException pe) { 201 throw (IllegalArgumentException)new IllegalArgumentException( 202 "Invalid errorField list '" + errorField + "'").initCause(pe); 203 } 204 } 205 super.add(new ErrorData(shortText, longText, errorField, severity)); 206 } 207 208 /** 209 * As per {@link #addError(String, String, String, int)}, with a default 210 * severity of {@link #SEVERITY_ERROR}. 211 * 212 * @param errorField a comma-separated list of field names that caused this error 213 * @param shortText a short string conveying the error type (e.g. 'Missing field') 214 * @param longText a longer string describing the nature of the error and how to resolve it 215 * (e.g. 'The field 'id' is mandatory. Please enter a value for this field.') 216 */ 217 public void addError(String errorField, String shortText, String longText) 218 { 219 addError(errorField, shortText, longText, SEVERITY_ERROR); 220 } 221 222 /** 223 * Adds an error that isn't associated with any particular field 224 * 225 * @param shortText a short string conveying the error type (e.g. 'Missing field') 226 * @param longText a longer string describing the nature of the error and how to resolve it 227 * (e.g. 'The field 'id' is mandatory. Please enter a value for this field.') 228 * @param severity the severity of this error (one of the SEVERITY_* constants of this class) 229 * 230 */ 231 public void addError(String shortText, String longText, int severity) 232 { 233 addError("", shortText, longText, severity); 234 } 235 236 /** 237 * Adds an error that isn't associated with any particular field, with a default 238 * severity of {@link #SEVERITY_ERROR}. 239 * 240 * @param shortText a short string conveying the error type (e.g. 'Missing field') 241 * @param longText a longer string describing the nature of the error and how to resolve it 242 * (e.g. 'The field 'id' is mandatory. Please enter a value for this field.') 243 */ 244 public void addError(String shortText, String longText) 245 { 246 addError("", shortText, longText, SEVERITY_ERROR); 247 } 248 249 /** 250 * Resets all errors contained within this object 251 */ 252 public void clearErrors() 253 { 254 super.clear(); 255 } 256 257 /** 258 * Appends the errors contained within another ErrorData into this one. 259 * 260 * @param errorList 261 * 262 * @return <b>true</b> if there were any additional errors to embed and at least 263 * one of the errors had a severity of SEVERITY_ERROR or higher (worse), 264 * <b>false</b> otherwise. 265 */ 266 public boolean addErrors(ErrorList errorList) 267 { 268 if (errorList == null) { 269 return false; 270 } 271 if (errorList.size() == 0) { 272 return false; 273 } 274 int maxSeverity = Integer.MIN_VALUE; 275 int severity; 276 277 for (int i = 0; i < errorList.size(); i++) { 278 severity = errorList.getSeverityAt(i); 279 add(errorList.get(i)); 280 if (severity > maxSeverity) { 281 maxSeverity = severity; 282 } 283 } 284 285 if (maxSeverity >= SEVERITY_ERROR) { 286 return true; 287 } else { 288 return false; 289 } 290 } 291 292 /** 293 * Returns true if an error has occured on the CGI field 294 * passed as a parameter to this method 295 * 296 * @return true if an error occured, false if not. 297 */ 298 public boolean hasErrorOn(String field) 299 { 300 ErrorData errorInfo; 301 302 for (Iterator<ErrorData> i = super.iterator(); i.hasNext(); ) { 303 errorInfo = i.next(); 304 if (errorInfo.getField()!=null) { 305 String[] errorFields = errorInfo.getField().split(","); 306 for (int j = 0; j < errorFields.length; j++) { 307 if (errorFields[j].equals(field)) { 308 return true; 309 } 310 } 311 } 312 } 313 return false; 314 } 315 316 /** 317 * Returns true if there are any errors in this object 318 * 319 * @return True if the number of errors > 0 320 */ 321 public boolean hasErrors() 322 { 323 return size() > 0; 324 } 325 326 /** 327 * Returns true if there are any errors of the specified severity or higher 328 * 329 * @param severity a SEVERITY_* constant 330 * 331 * @return True if the number of errors at the specified severity or higher > 0 332 */ 333 public boolean hasErrors(int severity) 334 { 335 for (Iterator<ErrorData> i = this.iterator(); i.hasNext(); ){ 336 ErrorData errorData = (ErrorData) i.next(); 337 if (errorData.getSeverity() >= severity ) { 338 return true; 339 } 340 } 341 return false; 342 } 343 344 345 /** 346 * Returns the maximum severity of all current errors. 347 * 348 * @return the severity ranking of the most severe error, or -1 if 349 * there are no errors contained within this ErrorData object. 350 */ 351 public int maxErrorSeverity() 352 { 353 int maxSeverity = -1; 354 int curSeverity; 355 356 for (int i = 0; i < size(); i++) { 357 curSeverity = getSeverityAt(i); 358 if (curSeverity > maxSeverity) { 359 maxSeverity = curSeverity; 360 } 361 } 362 return maxSeverity; 363 } 364 365 /** 366 * Returns the number of errors in this object 367 * 368 * @return The number of errors in this object 369 */ 370 public int size() { 371 return super.size(); 372 } 373 374 /** 375 * Same as {@link #size()}. Only here to allow us to access the object in JSTL 376 * 377 * @return The number of errors in this object 378 */ 379 public int getSize() { 380 return size(); 381 } 382 383 /** 384 * Returns the shortText string of the pos'th error 385 */ 386 public String getShortTextAt(int pos) { 387 return ((ErrorData)super.get(pos)).getShortText(); 388 } 389 390 /** 391 * Returns the longText of the pos'th error 392 */ 393 public String getLongTextAt(int pos) { 394 return ((ErrorData)super.get(pos)).getLongText(); 395 } 396 397 /** 398 * Returns the field of the pos'th error 399 */ 400 public String getFieldAt(int pos) { 401 return ((ErrorData)super.get(pos)).getField(); 402 } 403 404 /** 405 * Returns the severity of the pos'th error 406 */ 407 public int getSeverityAt(int pos) { 408 return ((ErrorData)super.get(pos)).getSeverity(); 409 } 410 411 /** 412 * Return all the errors within this object in a single string. 413 * Suitable for inclusion within email alarms, logs etc... 414 * 415 * @return Newline-separated list of errors 416 */ 417 public String toString() { 418 StringBuffer sb = new StringBuffer(); 419 ErrorData errorData; 420 for (Iterator<ErrorData> i = super.iterator(); i.hasNext(); ) { 421 errorData = (ErrorData)i.next(); 422 423 sb.append("#"); 424 sb.append(errorData.getShortText() + " - "); 425 sb.append(errorData.getLongText()); 426 if (!Text.isBlank(errorData.getField())) { 427 sb.append(" [" + errorData.getField() + "]\n"); 428 } 429 } 430 return sb.toString(); 431 } 432 433 /** Attaches an object to be validated to this ErrorList. 434 * 435 * @param attachedObject a POJO, a HttpServletRequest, or a Map 436 * (e.g. request.getParameterMap()). It identifies where the validated data 437 * is coming from. 438 * @param attachedBundle a bundle where the field names for this object are 439 * to be retrieved from. 440 * @param bundleFormat a MessageFormat used to to retrieve field names from 441 * attachedBundle. The "{0}" placeholder in this String is replaced with 442 * the name of the field being validated. 443 */ 444 445 // first parameter of this method can be 446 // second method 447 public void setValidatedObject(Object attachedObject, String attachedFieldFormat, 448 ResourceBundle attachedBundle, String bundleFormat, Locale locale) 449 { 450 if (attachedObject==null) { throw new NullPointerException("null attachedObject"); } 451 if (attachedFieldFormat==null) { throw new NullPointerException("null fieldFormat"); } 452 if (attachedBundle==null) { throw new NullPointerException("null attachedBundle"); } 453 if (bundleFormat==null) { throw new NullPointerException("null bundleFormat"); } 454 if (locale==null) { throw new NullPointerException("null locale"); } 455 456 this.attachedObject = attachedObject; 457 this.attachedFieldFormat = attachedFieldFormat; 458 this.attachedBundle = attachedBundle; 459 this.bundleFormat = bundleFormat; 460 this.locale = locale; 461 } 462 463 public void resetValidatedObject() 464 { 465 this.attachedObject = null; 466 this.attachedFieldFormat = null; 467 this.attachedBundle = null; 468 this.bundleFormat = null; 469 } 470 471 /** 472 * The object containing the data being validated 473 * 474 * @return the object containing the data being validated 475 */ 476 public Object getObject() 477 { 478 return attachedObject; 479 } 480 481 /** 482 * The locale in which validation messages will be localised 483 * 484 * @return The locale in which validation messages will be localised 485 */ 486 public Locale getLocale() 487 { 488 return locale; 489 } 490 491 /** Returns the value of a field from the attached object 492 * 493 * @param name field name 494 * 495 * @return field value 496 */ 497 public String getFieldValue(String name) { 498 // use reflection 499 if (attachedObject instanceof HttpServletRequest) { 500 return ((HttpServletRequest)attachedObject).getParameter(name); 501 } 502 503 Object obj = Struct.getValue(attachedObject, name); 504 if (obj == null) { 505 return null; 506 } else if (obj instanceof String[]) { 507 return ((String[])obj)[0]; 508 } else if (obj instanceof String) { 509 return (String)obj; 510 } else { 511 throw new IllegalStateException("Cannot retrieve non-string value '" + name + 512 "' (found class " + obj.getClass().getName() + ")"); 513 } 514 } 515 516 /** 517 * Returns the bundle used for localising validation messages 518 * 519 * @return the bundle used for localising validation messages 520 */ 521 public ResourceBundle getBundle() 522 { 523 return attachedBundle; 524 } 525 526 /** 527 * Returns a localised form of the supplied field name, to be used within 528 * generic validation messages 529 * 530 * @param field field name 531 * 532 * @return localised form of field name 533 */ 534 public String getLocalisedFieldName(String field) 535 { 536 return attachedBundle.getString(Text.replaceString(bundleFormat, "{0}", field)); 537 } 538 539 public String getFieldName(String field) 540 { 541 if (attachedFieldFormat == null) { 542 return field; 543 } else { 544 return Text.replaceString(attachedFieldFormat, "{0}", field); 545 } 546 } 547 548 /** Removes validation metadata from this object 549 * (i.e. attached objects, bundles, bundleFormats) 550 * 551 */ 552 public void unattach() 553 { 554 this.attachedObject = null; 555 this.attachedBundle = null; 556 this.bundleFormat = null; 557 } 558 559 560 /** Return the JSON representation of this object 561 * 562 * @return the JSON representation of this object 563 */ 564 public String toJSON() { 565 StringBuffer sb = new StringBuffer(); 566 ErrorList.ErrorData errorData; 567 sb.append("["); 568 for (int i=0; i<this.size(); i++) { 569 errorData = (ErrorData) get(i); 570 if (i>0) { sb.append(","); } 571 sb.append( 572 "{\"shortText\":\"" + Text.escapeJavascript(errorData.getShortText()) + "\"," + 573 "\"longText\":\"" + Text.escapeJavascript(errorData.getLongText()) + "\"," + 574 "\"fields\":\"" + Text.escapeJavascript(errorData.getField()) + "\"," + 575 "\"severity\":" + errorData.getSeverity() + "}"); 576 } 577 sb.append("]"); 578 return sb.toString(); 579 } 580 581}