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 &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}