View Javadoc
1   package com.randomnoun.common;
2   
3   import java.io.Serializable;
4   import java.util.ArrayList;
5   import java.util.Collection;
6   import java.util.Collections;
7   import java.util.Comparator;
8   import java.util.HashMap;
9   import java.util.HashSet;
10  import java.util.Iterator;
11  import java.util.List;
12  import java.util.ListIterator;
13  import java.util.Set;
14  import java.util.Spliterator;
15  import java.util.function.Consumer;
16  import java.util.function.Predicate;
17  import java.util.function.UnaryOperator;
18  import java.util.stream.Stream;
19  
20  /**
21   * The ErrorList class is a a fairly generic class for containing validation errors for
22   * input forms, similar to the struts ActionErrors class. 
23   * 
24   * <p>Each error within this class can contain a 'short' and 'long' description, 
25   * where the short description is normally two-word categorisation of the error 
26   * and the longer description describes what has happened and how to fix the problem; 
27   * (e.g. shortText="Missing field", longText="The field 'id' is mandatory. Please enter a value for this field."). 
28   * 
29   * <p>In the UI, the short description is normally rendered in bold before the long
30   * description. 
31   * 
32   * <p>An individual error also may contain a severity, 
33   * and list of fields that it applies to (defined by a comma-separate string of field names)
34   * An error without a severity supplied is assumed to be {@link #SEVERITY_INVALID}, 
35   * and an error without any fields supplied is assumed to be associated with the entire form, 
36   * rather than a specific set of fields.
37   * 
38   * <p>Errors are inserted into an ErrorList by using one of the addError methods:
39   * <ul>
40   * <li> {@link #addError(String, String)} - add an invalid form error
41   * <li> {@link #addError(String, String, int)} - add an form error with a specific severity
42   * <li> {@link #addError(String, String, String)} - add an invalid field error  
43   * <li> {@link #addError(String, String, String, int)} - add an invalid field error with a specific severity 
44   * </ul>
45   * 
46   * @author knoxg
47   */
48  public class ErrorList implements List<ErrorList.ErrorData>, Serializable {
49  
50  	/** Generated serialVersionUID */
51  	private static final long serialVersionUID = 4246736116021572339L;
52  	
53  	// uses an ArrayList by default, but by calling makeThreadsafe(), will wrap the backing array 
54  	// in a Collections.synchronizedList()
55  	List<ErrorList.ErrorData> delegate;
56  	boolean threadsafe = false;
57  	
58  	/** Severity level indicating 'not an error' (e.g. informational only). Used to indicate successful operations */
59      public static final int SEVERITY_OK = 0; // Not an error
60  
61      /** invalid user-supplied data; end-user can fix situation */
62      public static final int SEVERITY_INVALID = 1;
63  
64      /** invalid system-supplied data; end-user cannot fix situation */
65      public static final int SEVERITY_ERROR = 2;
66  
67      /** internal error (e.g. EJB/LDAP connection failure) */
68      public static final int SEVERITY_FATAL = 3;
69  
70      /** unrecoverable internal error (can't think of anything here, but the world ending could be one */
71      public static final int SEVERITY_PANIC = 4;
72  
73      // these were created after the 5 above, which is why they have higher ID numbers
74      // at a later stage will renumber these so that
75      // OK < INFO < WARNING < INVALID < ERROR < FATAL < PANIC
76      
77      /** information message; can be used in addition to SEVERITY_OK for additional text */
78      public static final int SEVERITY_INFO = 5;
79  
80      /** possibly incorrect user-supplied data; operation still succeeds but may return incorrect results */
81      public static final int SEVERITY_WARNING = 6;
82  
83      
84      /** ErrorInfo inner class - contains information related to
85       *  a single error
86       */
87      public static class ErrorData extends HashMap<String, Object>
88      {
89          /** Generated serialVersionUID */
90  		private static final long serialVersionUID = -176737615399095851L;
91  
92  		/**
93           * Creates a new ErrorInfo object.
94           *
95           * @param shortText a short, descriptive string for this error
96           * @param longText  a more lengthy description of this error
97           * @param errorField    comma-separated list of field names that caused
98           *                    this error
99           * @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 &gt; 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 &gt; 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 }