001: /*
002: * Copyright 2005-2007 The Kuali Foundation.
003: *
004: * Licensed under the Educational Community License, Version 1.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.opensource.org/licenses/ecl1.php
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.kuali.core.util;
017:
018: import java.io.Serializable;
019: import java.util.ArrayList;
020: import java.util.Collection;
021: import java.util.HashMap;
022: import java.util.Iterator;
023: import java.util.List;
024: import java.util.Map;
025: import java.util.Set;
026:
027: import org.apache.commons.lang.StringUtils;
028: import org.kuali.RiceConstants;
029:
030: /**
031: * Holds errors due to validation. Keys of map represent property paths, and value is a TypedArrayList that contains resource string
032: * keys (to retrieve the error message).
033: *
034: *
035: */
036: public class ErrorMap implements Map, Serializable {
037: private static final long serialVersionUID = -2328635367656516150L;
038: private List errorPath = new ArrayList();
039: private Map messages = new HashMap();
040:
041: /**
042: * Adds an error to the map under the given propertyName and adds an array of message parameters. This will fully prepend the
043: * error key with any value in the errorPath list. This should be used when you do not want to add the error with the prepend
044: * pre-built error path.
045: *
046: * @param propertyName name of the property to add error under
047: * @param errorKey resource key used to retrieve the error text from the error message resource bundle
048: * @param errorParameters zero or more string parameters for the displayed error message
049: * @return TypedArrayList
050: */
051: public TypedArrayList putError(String propertyName,
052: String errorKey, String... errorParameters) {
053: return putError(propertyName, errorKey, true, errorParameters);
054: }
055:
056: /**
057: * Adds an error to the map under the given propertyName and adds an array of message parameters. This will fully prepend the
058: * error key with any value in the errorPath list.
059: *
060: * @param propertyName name of the property to add error under
061: * @param errorKey resource key used to retrieve the error text from the error message resource bundle
062: * @param errorParameters zero or more string parameters for the displayed error message
063: * @return TypedArrayList
064: */
065: public TypedArrayList putErrorWithoutFullErrorPath(
066: String propertyName, String errorKey,
067: String... errorParameters) {
068: return putError(propertyName, errorKey, false, errorParameters);
069: }
070:
071: /**
072: * adds an error to the map under the given propertyName and adds an array of message parameters.
073: *
074: * @param propertyName name of the property to add error under
075: * @param errorKey resource key used to retrieve the error text from the error message resource bundle
076: * @param withFullErrorPath true if you want the whole parent error path appended, false otherwise
077: * @param errorParameters zero or more string parameters for the displayed error message
078: * @return TypeArrayList
079: */
080: private TypedArrayList putError(String propertyName,
081: String errorKey, boolean withFullErrorPath,
082: String... errorParameters) {
083: if (StringUtils.isBlank(propertyName)) {
084: throw new IllegalArgumentException(
085: "invalid (blank) propertyName");
086: }
087: if (StringUtils.isBlank(errorKey)) {
088: throw new IllegalArgumentException(
089: "invalid (blank) errorKey");
090: }
091:
092: // check if we have previous errors for this property
093: TypedArrayList errorList = null;
094: String propertyKey = getKeyPath((String) propertyName,
095: withFullErrorPath);
096: if (messages.containsKey(propertyKey)) {
097: errorList = (TypedArrayList) messages.get(propertyKey);
098: } else {
099: errorList = new TypedArrayList(ErrorMessage.class);
100: }
101:
102: // add error to list
103: ErrorMessage errorMessage = new ErrorMessage(errorKey,
104: errorParameters);
105: // check if this error has already been added to the list
106: if (!errorList.contains(errorMessage)) {
107: errorList.add(errorMessage);
108: }
109:
110: return (TypedArrayList) messages.put(propertyKey, errorList);
111: }
112:
113: /**
114: * If any error messages with the key targetKey exist in this ErrorMap for the named property, those ErrorMessages will be
115: * replaced with a new ErrorMessage with the given replaceKey and replaceParameters.
116: *
117: * @param propertyName name of the property where existing error will be replaced
118: * @param targetKey error key of message to be replaced
119: * @paran replaceKey error key which will replace targetKey
120: * @param replaceParameters zero or more string parameters for the replacement error message
121: * @return true if the replacement occurred
122: */
123: public boolean replaceError(String propertyName, String targetKey,
124: String replaceKey, String... replaceParameters) {
125: return replaceError(propertyName, targetKey, true, replaceKey,
126: replaceParameters);
127: }
128:
129: /**
130: * If any error messages with the key targetKey exist in this ErrorMap for the named property, those ErrorMessages will be
131: * replaced with a new ErrorMessage with the given replaceKey and replaceParameters. The targetKey and replaceKey will be
132: * prepended with the current errorPath, if any.
133: *
134: *
135: * @param propertyName name of the property where existing error will be replaced
136: * @param targetKey error key of message to be replaced
137: * @paran replaceKey error key which will replace targetKey
138: * @param replaceParameters zero or more string parameters for the replacement error message
139: * @return true if the replacement occurred
140: */
141: public boolean replaceErrorWithoutFullErrorPath(
142: String propertyName, String targetKey, String replaceKey,
143: String... replaceParameters) {
144: return replaceError(propertyName, targetKey, false, replaceKey,
145: replaceParameters);
146: }
147:
148: /**
149: * If any error messages with the key targetKey exist in this ErrorMap for the named property, those ErrorMessages will be
150: * replaced with a new ErrorMessage with the given replaceKey and replaceParameters.
151: *
152: * @param propertyName name of the property to add error under
153: * @param errorKey resource key used to retrieve the error text
154: * @param withFullErrorPath true if you want the whole parent error path appended, false otherwise
155: * @param errorParameters zero or more string parameters for the displayed error message
156: * @return true if the replacement occurred
157: */
158: private boolean replaceError(String propertyName, String targetKey,
159: boolean withFullErrorPath, String replaceKey,
160: String... replaceParameters) {
161: boolean replaced = false;
162:
163: if (StringUtils.isBlank(propertyName)) {
164: throw new IllegalArgumentException(
165: "invalid (blank) propertyName");
166: }
167: if (StringUtils.isBlank(targetKey)) {
168: throw new IllegalArgumentException(
169: "invalid (blank) targetKey");
170: }
171: if (StringUtils.isBlank(replaceKey)) {
172: throw new IllegalArgumentException(
173: "invalid (blank) replaceKey");
174: }
175:
176: // check if we have previous errors for this property
177: TypedArrayList errorList = null;
178: String propertyKey = getKeyPath((String) propertyName,
179: withFullErrorPath);
180: if (messages.containsKey(propertyKey)) {
181: errorList = (TypedArrayList) messages.get(propertyKey);
182:
183: // look for the specific targetKey
184: for (int i = 0; i < errorList.size(); ++i) {
185: ErrorMessage em = (ErrorMessage) errorList.get(i);
186:
187: // replace matching messages
188: if (em.getErrorKey().equals(targetKey)) {
189: ErrorMessage rm = new ErrorMessage(replaceKey,
190: replaceParameters);
191: errorList.set(i, rm);
192: replaced = true;
193: }
194: }
195: }
196:
197: return replaced;
198: }
199:
200: /**
201: * Returns true if the named field has a message with the given errorKey
202: *
203: * @param errorKey
204: * @param fieldName
205: * @return boolean
206: */
207: public boolean fieldHasMessage(String fieldName, String errorKey) {
208: boolean found = false;
209:
210: List fieldMessages = (List) messages.get(fieldName);
211: if (fieldMessages != null) {
212: for (Iterator i = fieldMessages.iterator(); !found
213: && i.hasNext();) {
214: ErrorMessage errorMessage = (ErrorMessage) i.next();
215: found = errorMessage.getErrorKey().equals(errorKey);
216: }
217: }
218:
219: return found;
220: }
221:
222: /**
223: * Returns the number of messages for the given field
224: *
225: * @param fieldName
226: * @return int
227: */
228: public int countFieldMessages(String fieldName) {
229: int count = 0;
230:
231: List fieldMessages = (List) messages.get(fieldName);
232: if (fieldMessages != null) {
233: count = fieldMessages.size();
234: }
235:
236: return count;
237: }
238:
239: /**
240: * @return true if the given messageKey is associated with some property in this ErrorMap
241: */
242: public boolean containsMessageKey(String messageKey) {
243: ErrorMessage foundMessage = null;
244:
245: if (!isEmpty()) {
246: for (Iterator i = entrySet().iterator(); (foundMessage == null)
247: && i.hasNext();) {
248: Map.Entry e = (Map.Entry) i.next();
249: String entryKey = (String) e.getKey();
250: TypedArrayList entryErrorList = (TypedArrayList) e
251: .getValue();
252: for (Iterator j = entryErrorList.iterator(); j
253: .hasNext();) {
254: ErrorMessage em = (ErrorMessage) j.next();
255: if (messageKey.equals(em.getErrorKey())) {
256: foundMessage = em;
257: }
258: }
259: }
260: }
261:
262: return (foundMessage != null);
263: }
264:
265: /**
266: * Counts the total number of error messages in the map
267: *
268: * @return returns an int for the total number of errors
269: */
270: public int getErrorCount() {
271: int errorCount = 0;
272: for (Iterator iter = messages.keySet().iterator(); iter
273: .hasNext();) {
274: String errorKey = (String) iter.next();
275: List errors = (List) messages.get(errorKey);
276: errorCount += errors.size();
277: }
278:
279: return errorCount;
280: }
281:
282: /**
283: * @param path
284: * @return Returns a List of ErrorMessages for the given path
285: */
286: public TypedArrayList getMessages(String path) {
287: return (TypedArrayList) messages.get(path);
288: }
289:
290: /**
291: * Adds a string prefix to the error path.
292: *
293: * @param parentName
294: */
295: public void addToErrorPath(String parentName) {
296: errorPath.add(parentName);
297: }
298:
299: /**
300: * This method returns the list that holds the error path values.
301: *
302: * @return List
303: */
304: public List getErrorPath() {
305: return errorPath;
306: }
307:
308: /**
309: * Removes a string prefix from the error path.
310: *
311: * @param parentName
312: * @return boolean Returns true if the parentName existed, false otherwise.
313: */
314: public boolean removeFromErrorPath(String parentName) {
315: return errorPath.remove(parentName);
316: }
317:
318: /**
319: * Clears the errorPath.
320: */
321: public void clearErrorPath() {
322: errorPath = new ArrayList();
323: }
324:
325: /**
326: * This is what's prepended to the beginning of the key. This is built by iterating over all of the entries in the errorPath
327: * list and concatenating them together witha "."
328: *
329: * @return String Returns the keyPath.
330: * @param propertyName
331: * @param prependFullErrorPath
332: */
333: public String getKeyPath(String propertyName,
334: boolean prependFullErrorPath) {
335: String keyPath = "";
336:
337: if (RiceConstants.GLOBAL_ERRORS.equals(propertyName)) {
338: return RiceConstants.GLOBAL_ERRORS;
339: }
340:
341: if (!errorPath.isEmpty() && prependFullErrorPath) {
342: keyPath = StringUtils.join(errorPath.iterator(), ".");
343: keyPath += "." + propertyName;
344: } else {
345: keyPath = propertyName;
346: }
347:
348: return keyPath;
349: }
350:
351: /**
352: * @return List of the property names that have errors.
353: */
354: public List getPropertiesWithErrors() {
355: List properties = new ArrayList();
356:
357: for (Iterator iter = messages.keySet().iterator(); iter
358: .hasNext();) {
359: properties.add(iter.next());
360: }
361:
362: return properties;
363: }
364:
365: // methods added to complete the Map interface
366: /**
367: * Clears the messages list.
368: */
369: public void clear() {
370: messages.clear();
371: }
372:
373: /**
374: * @see java.util.Map#containsKey(java.lang.Object)
375: */
376: public boolean containsKey(Object key) {
377: return messages.containsKey(key);
378: }
379:
380: /**
381: * @param pattern comma separated list of keys, optionally ending with * wildcard
382: */
383: public boolean containsKeyMatchingPattern(String pattern) {
384: ArrayList simplePatterns = new ArrayList();
385: ArrayList wildcardPatterns = new ArrayList();
386: String[] patterns = pattern.split(",");
387: for (int i = 0; i < patterns.length; i++) {
388: String s = patterns[i];
389: if (s.endsWith("*")) {
390: wildcardPatterns.add(s.substring(0, s.length() - 1));
391: } else {
392: simplePatterns.add(s);
393: }
394: }
395: for (Iterator keys = messages.keySet().iterator(); keys
396: .hasNext();) {
397: String key = (String) keys.next();
398: if (simplePatterns.contains(key)) {
399: return true;
400: }
401: for (Iterator wildcardIterator = wildcardPatterns
402: .iterator(); wildcardIterator.hasNext();) {
403: String wildcard = (String) wildcardIterator.next();
404: if (key.startsWith(wildcard)) {
405: return true;
406: }
407: }
408: }
409: return false;
410: }
411:
412: /**
413: * @see java.util.Map#entrySet()
414: */
415: public Set entrySet() {
416: return messages.entrySet();
417: }
418:
419: /**
420: * @see java.util.Map#get(java.lang.Object)
421: */
422: public Object get(Object key) {
423: return messages.get(key);
424: }
425:
426: /**
427: * @see java.util.Map#isEmpty()
428: */
429: public boolean isEmpty() {
430: return messages.isEmpty();
431: }
432:
433: /**
434: * @see java.util.Map#keySet()
435: */
436: public Set keySet() {
437: return messages.keySet();
438: }
439:
440: /**
441: * @see java.util.Map#remove(java.lang.Object)
442: */
443: public Object remove(Object key) {
444: return messages.remove(key);
445: }
446:
447: /**
448: * @see java.util.Map#size()
449: */
450: public int size() {
451: return messages.size();
452: }
453:
454: // forbidden-but-required operations
455: /**
456: * Prevent people from adding arbitrary objects to the messages Map
457: *
458: * @param key
459: * @param value
460: * @return Object
461: */
462: public Object put(Object key, Object value) {
463: throw new UnsupportedOperationException();
464: }
465:
466: /**
467: * Prevent people from adding arbitrary objects to the messages Map
468: *
469: * @param arg0
470: */
471: public void putAll(Map arg0) {
472: throw new UnsupportedOperationException();
473: }
474:
475: /**
476: * Prevent people from directly accessing the values, since the input parameter isn't strongly-enough typed
477: *
478: * @param value
479: * @return boolean
480: */
481: public boolean containsValue(Object value) {
482: throw new UnsupportedOperationException();
483: }
484:
485: /**
486: * Prevent people from directly accessing the values, since the input parameter isn't strongly-enough typed
487: *
488: * @return Collection
489: */
490: public Collection values() {
491: throw new UnsupportedOperationException();
492: }
493:
494: /**
495: * Renders as a String, to help debug tests.
496: *
497: * @return a String, to help debug tests.
498: */
499: @Override
500: public String toString() {
501: return "ErrorMap (errorPath = " + errorPath + ", messages = "
502: + messages + ")";
503: }
504:
505: /**
506: * @see java.lang.Object#equals(java.lang.Object)
507: */
508: @Override
509: public boolean equals(Object obj) {
510: boolean equals = false;
511:
512: if (this == obj) {
513: equals = true;
514: } else if (obj instanceof ErrorMap) {
515: ErrorMap other = (ErrorMap) obj;
516:
517: if (getErrorPath().equals(other.getErrorPath())) {
518: if (size() == other.size()) {
519: if (entrySet().equals(other.entrySet())) {
520: equals = true;
521: }
522: }
523: }
524: }
525:
526: return equals;
527: }
528:
529: /**
530: * Returns the size, since that meets with the requirements of the hashCode contract, and since I don't expect ErrorMap to be
531: * used as the key in a Map.
532: *
533: * @see java.lang.Object#hashCode()
534: */
535: @Override
536: public int hashCode() {
537: return size();
538: }
539: }
|