001package com.randomnoun.common; 002 003import java.io.Serializable; 004import java.util.ArrayList; 005import java.util.Collection; 006import java.util.Collections; 007import java.util.Comparator; 008import java.util.HashMap; 009import java.util.HashSet; 010import java.util.Iterator; 011import java.util.List; 012import java.util.ListIterator; 013import java.util.Set; 014import java.util.Spliterator; 015import java.util.function.Consumer; 016import java.util.function.Predicate; 017import java.util.function.UnaryOperator; 018import java.util.stream.Stream; 019 020/** 021 * The ErrorList class is a a fairly generic class for containing validation errors for 022 * input forms, similar to the struts ActionErrors class. 023 * 024 * <p>Each error within this class can contain a 'short' and 'long' description, 025 * where the short description is normally two-word categorisation of the error 026 * and the longer description describes what has happened and how to fix the problem; 027 * (e.g. shortText="Missing field", longText="The field 'id' is mandatory. Please enter a value for this field."). 028 * 029 * <p>In the UI, the short description is normally rendered in bold before the long 030 * description. 031 * 032 * <p>An individual error also may contain a severity, 033 * and list of fields that it applies to (defined by a comma-separate string of field names) 034 * An error without a severity supplied is assumed to be {@link #SEVERITY_INVALID}, 035 * and an error without any fields supplied is assumed to be associated with the entire form, 036 * rather than a specific set of fields. 037 * 038 * <p>Errors are inserted into an ErrorList by using one of the addError methods: 039 * <ul> 040 * <li> {@link #addError(String, String)} - add an invalid form error 041 * <li> {@link #addError(String, String, int)} - add an form error with a specific severity 042 * <li> {@link #addError(String, String, String)} - add an invalid field error 043 * <li> {@link #addError(String, String, String, int)} - add an invalid field error with a specific severity 044 * </ul> 045 * 046 * @author knoxg 047 */ 048public class ErrorList implements List<ErrorList.ErrorData>, Serializable { 049 050 /** Generated serialVersionUID */ 051 private static final long serialVersionUID = 4246736116021572339L; 052 053 // uses an ArrayList by default, but by calling makeThreadsafe(), will wrap the backing array 054 // in a Collections.synchronizedList() 055 List<ErrorList.ErrorData> delegate; 056 boolean threadsafe = false; 057 058 /** Severity level indicating 'not an error' (e.g. informational only). Used to indicate successful operations */ 059 public static final int SEVERITY_OK = 0; // Not an error 060 061 /** invalid user-supplied data; end-user can fix situation */ 062 public static final int SEVERITY_INVALID = 1; 063 064 /** invalid system-supplied data; end-user cannot fix situation */ 065 public static final int SEVERITY_ERROR = 2; 066 067 /** internal error (e.g. EJB/LDAP connection failure) */ 068 public static final int SEVERITY_FATAL = 3; 069 070 /** unrecoverable internal error (can't think of anything here, but the world ending could be one */ 071 public static final int SEVERITY_PANIC = 4; 072 073 // these were created after the 5 above, which is why they have higher ID numbers 074 // at a later stage will renumber these so that 075 // OK < INFO < WARNING < INVALID < ERROR < FATAL < PANIC 076 077 /** information message; can be used in addition to SEVERITY_OK for additional text */ 078 public static final int SEVERITY_INFO = 5; 079 080 /** possibly incorrect user-supplied data; operation still succeeds but may return incorrect results */ 081 public static final int SEVERITY_WARNING = 6; 082 083 084 /** ErrorInfo inner class - contains information related to 085 * a single error 086 */ 087 public static class ErrorData 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 @Override 112 public int hashCode() { 113 return super.hashCode(); 114 } 115 116 @Override 117 public boolean equals(Object obj) { 118 return super.equals(obj); 119 } 120 121 /** Retrieves the type for this error 122 * @return the type for this error 123 */ 124 public String getShortText() 125 { 126 return (String) get("shortText"); 127 } 128 129 /** Retrieves the description for this error 130 * @return the description for this error 131 */ 132 public String getLongText() 133 { 134 return (String) get("longText"); 135 } 136 137 /** Retrieves a comma-separated list of fields that caused this error 138 * @return a comma-separated list of fields that caused this error 139 */ 140 public String getField() 141 { 142 return (String) get("field"); 143 } 144 145 /** Retrieves the severity of this error 146 * @return the severity of this error 147 */ 148 public int getSeverity() 149 { 150 return ((Integer) get("severity")).intValue(); 151 } 152 153 /** Retrieves a string representation of this error 154 * @return a string representation of this error 155 */ 156 public String toString() 157 { 158 return "{shortText='" + getShortText() + "', longText='" + getLongText() + 159 "', fields='" + getField() + "', severity=" + getSeverity() + "}"; 160 } 161 } 162 163 /** 164 * Create a new, empty ErrorData object. 165 */ 166 public ErrorList() 167 { 168 delegate = new ArrayList<>(); 169 } 170 171 172 /** Convert the backing store for this ErrorList to a Collections.synchronizedList, suitable for use in multithreaded applications. 173 * Note that iterations on this collection must still be synchronised. 174 */ 175 public synchronized void makeThreadsafe() { 176 if (!threadsafe) { 177 delegate = Collections.synchronizedList(delegate); 178 threadsafe = true; 179 } 180 } 181 182 /** Removes any duplicate errors in this ErrorList */ 183 public void removeDuplicates() { 184 Set<ErrorList.ErrorData> uniqueData = new HashSet<>(); 185 synchronized(delegate) { 186 for (Iterator<ErrorList.ErrorData> i = this.iterator(); i.hasNext(); ) { 187 ErrorList.ErrorData ed = i.next(); 188 if (uniqueData.contains(ed)) { 189 i.remove(); 190 } else { 191 uniqueData.add(ed); 192 } 193 } 194 } 195 } 196 197 /** 198 * Adds an error. This is the "real" addError() method; all others delegate to this 199 * one. 200 * 201 * @param errorField a comma-separated list of field names that caused this error 202 * @param shortText a short string conveying the error type (e.g. 'Missing field') 203 * @param longText a longer string describing the nature of the error and how to resolve it 204 * (e.g. 'The field 'id' is mandatory. Please enter a value for this field.') 205 * @param severity the severity of this error (one of the SEVERITY_* constants of this class). 206 * 207 * @see SEVERITY_INVALID 208 */ 209 public void addError(String errorField, String shortText, String longText, 210 int severity) 211 { 212 delegate.add(new ErrorData(shortText, longText, errorField, severity)); 213 } 214 215 /** 216 * As per {@link #addError(String, String, String, int)}, with a default 217 * severity of {@link #SEVERITY_ERROR}. 218 * 219 * @param errorField a comma-separated list of field names that caused this error 220 * @param shortText a short string conveying the error type (e.g. 'Missing field') 221 * @param longText a longer string describing the nature of the error and how to resolve it 222 * (e.g. 'The field 'id' is mandatory. Please enter a value for this field.') 223 */ 224 public void addError(String errorField, String shortText, String longText) 225 { 226 addError(errorField, shortText, longText, SEVERITY_ERROR); 227 } 228 229 /** 230 * Adds an error that isn't associated with any particular field 231 * 232 * @param shortText a short string conveying the error type (e.g. 'Missing field') 233 * @param longText a longer string describing the nature of the error and how to resolve it 234 * (e.g. 'The field 'id' is mandatory. Please enter a value for this field.') 235 * @param severity the severity of this error (one of the SEVERITY_* constants of this class) 236 * 237 */ 238 public void addError(String shortText, String longText, int severity) 239 { 240 addError("", shortText, longText, severity); 241 } 242 243 /** 244 * Adds an error that isn't associated with any particular field, with a default 245 * severity of {@link #SEVERITY_ERROR}. 246 * 247 * @param shortText a short string conveying the error type (e.g. 'Missing field') 248 * @param longText a longer string describing the nature of the error and how to resolve it 249 * (e.g. 'The field 'id' is mandatory. Please enter a value for this field.') 250 */ 251 public void addError(String shortText, String longText) 252 { 253 addError("", shortText, longText, SEVERITY_ERROR); 254 } 255 256 /** 257 * Resets all errors contained within this object 258 */ 259 public void clearErrors() 260 { 261 delegate.clear(); 262 } 263 264 /** 265 * Appends the errors contained within another ErrorData into this one. 266 * 267 * @param errorList 268 * 269 * @return <b>true</b> if there were any additional errors to embed and at least 270 * one of the errors had a severity of SEVERITY_ERROR or higher (worse), 271 * <b>false</b> otherwise. 272 */ 273 public boolean addErrors(ErrorList errorList) 274 { 275 if (errorList == null) { 276 return false; 277 } 278 if (errorList.size() == 0) { 279 return false; 280 } 281 int maxSeverity = Integer.MIN_VALUE; 282 int severity; 283 284 for (int i = 0; i < errorList.size(); i++) { 285 severity = errorList.getSeverityAt(i); 286 add(errorList.get(i)); 287 if (severity > maxSeverity) { 288 maxSeverity = severity; 289 } 290 } 291 292 if (maxSeverity >= SEVERITY_ERROR) { 293 return true; 294 } else { 295 return false; 296 } 297 } 298 299 /** 300 * Returns true if an error has occured on the field 301 * passed as a parameter to this method 302 * 303 * @return true if an error occured, false if not. 304 */ 305 public boolean hasErrorOn(String field) 306 { 307 ErrorData errorInfo; 308 309 synchronized(delegate) { 310 for (Iterator<ErrorData> i = delegate.iterator(); i.hasNext(); ) { 311 errorInfo = i.next(); 312 if (errorInfo.getField()!=null) { 313 String[] errorFields = errorInfo.getField().split(","); 314 for (int j = 0; j < errorFields.length; j++) { 315 if (errorFields[j].equals(field)) { 316 return true; 317 } 318 } 319 } 320 } 321 } 322 return false; 323 } 324 325 /** 326 * Returns true if there are any errors in this object 327 * 328 * @return True if the number of errors > 0 329 */ 330 public boolean hasErrors() 331 { 332 return size() > 0; 333 } 334 335 /** 336 * Returns true if there are any errors of the specified severity or higher 337 * Note that our severities aren't in increasing severity order any more so this method is now deprecated 338 * 339 * @param severity a SEVERITY_* constant 340 * 341 * @return True if the number of errors at the specified severity or higher > 0 342 * @deprecated 343 */ 344 public boolean hasErrors(int severity) 345 { 346 synchronized(delegate) { 347 for (Iterator<ErrorData> i = this.iterator(); i.hasNext(); ){ 348 ErrorData errorData = (ErrorData) i.next(); 349 if (errorData.getSeverity() >= severity ) { 350 return true; 351 } 352 } 353 } 354 return false; 355 } 356 357 358 /** 359 * Returns the maximum severity of all current errors. 360 * Note that our severities aren't in increasing severity order any more so this method is now deprecated 361 * 362 * @return the severity ranking of the most severe error, or -1 if 363 * there are no errors contained within this ErrorData object. 364 * @deprecated 365 */ 366 public int maxErrorSeverity() 367 { 368 int maxSeverity = -1; 369 int curSeverity; 370 371 synchronized(delegate) { 372 for (int i = 0; i < size(); i++) { 373 curSeverity = getSeverityAt(i); 374 if (curSeverity > maxSeverity) { 375 maxSeverity = curSeverity; 376 } 377 } 378 } 379 return maxSeverity; 380 } 381 382 /** 383 * Returns the number of errors in this object 384 * 385 * @return The number of errors in this object 386 */ 387 public int size() { 388 return delegate.size(); 389 } 390 391 /** 392 * Same as {@link #size()}. Only here to allow us to access the object in JSTL 393 * 394 * @return The number of errors in this object 395 */ 396 public int getSize() { 397 return size(); 398 } 399 400 /** 401 * Returns the shortText string of the pos'th error 402 */ 403 public String getShortTextAt(int pos) { 404 return ((ErrorData) delegate.get(pos)).getShortText(); 405 } 406 407 /** 408 * Returns the longText of the pos'th error 409 */ 410 public String getLongTextAt(int pos) { 411 return ((ErrorData) delegate.get(pos)).getLongText(); 412 } 413 414 /** 415 * Returns the field of the pos'th error 416 */ 417 public String getFieldAt(int pos) { 418 return ((ErrorData) delegate.get(pos)).getField(); 419 } 420 421 /** 422 * Returns the severity of the pos'th error 423 */ 424 public int getSeverityAt(int pos) { 425 return ((ErrorData) delegate.get(pos)).getSeverity(); 426 } 427 428 /** 429 * Return all the errors within this object in a single string. 430 * Suitable for inclusion within email alarms, logs etc... 431 * 432 * @return Newline-separated list of errors 433 */ 434 public String toString() { 435 StringBuffer sb = new StringBuffer(); 436 ErrorData errorData; 437 synchronized(delegate) { 438 for (Iterator<ErrorData> i = delegate.iterator(); i.hasNext(); ) { 439 errorData = (ErrorData)i.next(); 440 441 sb.append("#"); 442 sb.append(errorData.getShortText() + " - "); 443 sb.append(errorData.getLongText()); 444 if (!Text.isBlank(errorData.getField())) { 445 sb.append(" [" + errorData.getField() + "]\n"); 446 } 447 } 448 } 449 return sb.toString(); 450 } 451 452 /** Return the JSON representation of this object 453 * 454 * @return the JSON representation of this object 455 */ 456 public String toJSON() { 457 StringBuffer sb = new StringBuffer(); 458 ErrorList.ErrorData errorData; 459 sb.append("["); 460 for (int i=0; i<this.size(); i++) { 461 errorData = (ErrorData) get(i); 462 if (i>0) { sb.append(","); } 463 sb.append( 464 "{\"shortText\":\"" + Text.escapeJavascript(errorData.getShortText()) + "\"," + 465 "\"longText\":\"" + Text.escapeJavascript(errorData.getLongText()) + "\"," + 466 "\"fields\":\"" + Text.escapeJavascript(errorData.getField()) + "\"," + 467 "\"severity\":" + errorData.getSeverity() + "}"); 468 } 469 sb.append("]"); 470 return sb.toString(); 471 } 472 473 /** Delegate methods */ 474 475 476 public void forEach(Consumer<? super ErrorData> action) { 477 delegate.forEach(action); 478 } 479 480 public boolean isEmpty() { 481 return delegate.isEmpty(); 482 } 483 484 public boolean contains(Object o) { 485 return delegate.contains(o); 486 } 487 488 public Iterator<ErrorData> iterator() { 489 return delegate.iterator(); 490 } 491 492 public Object[] toArray() { 493 return delegate.toArray(); 494 } 495 496 public <T> T[] toArray(T[] a) { 497 return delegate.toArray(a); 498 } 499 500 public boolean add(ErrorData e) { 501 return delegate.add(e); 502 } 503 504 public boolean remove(Object o) { 505 return delegate.remove(o); 506 } 507 508 public boolean containsAll(Collection<?> c) { 509 return delegate.containsAll(c); 510 } 511 512 public boolean addAll(Collection<? extends ErrorData> c) { 513 return delegate.addAll(c); 514 } 515 516 public boolean addAll(int index, Collection<? extends ErrorData> c) { 517 return delegate.addAll(index, c); 518 } 519 520 public boolean removeAll(Collection<?> c) { 521 return delegate.removeAll(c); 522 } 523 524 public boolean retainAll(Collection<?> c) { 525 return delegate.retainAll(c); 526 } 527 528 public void replaceAll(UnaryOperator<ErrorData> operator) { 529 delegate.replaceAll(operator); 530 } 531 532 public boolean removeIf(Predicate<? super ErrorData> filter) { 533 return delegate.removeIf(filter); 534 } 535 536 public void sort(Comparator<? super ErrorData> c) { 537 delegate.sort(c); 538 } 539 540 public void clear() { 541 delegate.clear(); 542 } 543 544 public boolean equals(Object o) { 545 return delegate.equals(o); 546 } 547 548 public int hashCode() { 549 return delegate.hashCode(); 550 } 551 552 public ErrorData get(int index) { 553 return delegate.get(index); 554 } 555 556 public ErrorData set(int index, ErrorData element) { 557 return delegate.set(index, element); 558 } 559 560 public void add(int index, ErrorData element) { 561 delegate.add(index, element); 562 } 563 564 public Stream<ErrorData> stream() { 565 return delegate.stream(); 566 } 567 568 public ErrorData remove(int index) { 569 return delegate.remove(index); 570 } 571 572 public Stream<ErrorData> parallelStream() { 573 return delegate.parallelStream(); 574 } 575 576 public int indexOf(Object o) { 577 return delegate.indexOf(o); 578 } 579 580 public int lastIndexOf(Object o) { 581 return delegate.lastIndexOf(o); 582 } 583 584 public ListIterator<ErrorData> listIterator() { 585 return delegate.listIterator(); 586 } 587 588 public ListIterator<ErrorData> listIterator(int index) { 589 return delegate.listIterator(index); 590 } 591 592 public List<ErrorData> subList(int fromIndex, int toIndex) { 593 return delegate.subList(fromIndex, toIndex); 594 } 595 596 public Spliterator<ErrorData> spliterator() { 597 return delegate.spliterator(); 598 } 599 600 601}