001: /*
002: * $Id: ValueMap.java 462298 2006-09-18 00:00:20Z ehillenius $ $Revision: 462298 $
003: * $Date: 2006-09-18 02:00:20 +0200 (Mon, 18 Sep 2006) $
004: *
005: * ==============================================================================
006: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
007: * use this file except in compliance with the License. You may obtain a copy of
008: * the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
014: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
015: * License for the specific language governing permissions and limitations under
016: * the License.
017: */
018: package wicket.util.value;
019:
020: import java.lang.reflect.Array;
021: import java.util.Arrays;
022: import java.util.HashMap;
023: import java.util.Iterator;
024: import java.util.Map;
025:
026: import wicket.util.parse.metapattern.MetaPattern;
027: import wicket.util.parse.metapattern.parsers.VariableAssignmentParser;
028: import wicket.util.string.IStringIterator;
029: import wicket.util.string.StringList;
030: import wicket.util.string.StringValue;
031: import wicket.util.string.StringValueConversionException;
032: import wicket.util.time.Duration;
033: import wicket.util.time.Time;
034:
035: /**
036: * A Map implementation that holds values, parses strings and exposes a variety
037: * of convenience methods.
038: * <p>
039: * In addition to a no-arg constructor and a copy constructor that takes a Map
040: * argument, ValueMaps can be constructed using a parsing constructor.
041: * ValueMap(String) will parse values from the string in comma separated
042: * key/value assignment pairs. For example, new ValueMap("a=9,b=foo").
043: * <p>
044: * Values can be retrieved from the map in the usual way or with methods that do
045: * handy conversions to various types, including String, StringValue, int, long,
046: * double, Time and Duration.
047: * <p>
048: * The makeImmutable method will make the underlying map immutable. Further
049: * attempts to change the map will result in a runtime exception.
050: * <p>
051: * The toString() method converts a ValueMap object to a readable key/value
052: * string for diagnostics.
053: *
054: * @author Jonathan Locke
055: */
056: public class ValueMap extends HashMap {
057: /** An empty ValueMap. */
058: public static final ValueMap EMPTY_MAP = new ValueMap();
059:
060: private static final long serialVersionUID = 1L;
061:
062: /** True if this value map has been made immutable. */
063: private boolean immutable = false;
064:
065: /**
066: * Constructs empty value map.
067: */
068: public ValueMap() {
069: }
070:
071: /**
072: * Copy constructor.
073: *
074: * @param map
075: * The map to copy
076: */
077: public ValueMap(final Map map) {
078: super .putAll(map);
079: }
080:
081: /**
082: * Constructor.
083: *
084: * @param keyValuePairs
085: * List of key value pairs separated by commas. For example,
086: * "param1=foo,param2=bar"
087: */
088: public ValueMap(final String keyValuePairs) {
089: this (keyValuePairs, ",");
090: }
091:
092: /**
093: * Constructor.
094: *
095: * @param keyValuePairs
096: * List of key value pairs separated by a given delimiter. For
097: * example, "param1=foo,param2=bar" where delimiter is ",".
098: * @param delimiter
099: * Delimiter string used to separate key/value pairs
100: */
101: public ValueMap(final String keyValuePairs, final String delimiter) {
102: int start = 0;
103: int equalsIndex = keyValuePairs.indexOf('=');
104: int delimiterIndex = keyValuePairs.indexOf(delimiter,
105: equalsIndex);
106: if (delimiterIndex == -1)
107: delimiterIndex = keyValuePairs.length();
108: while (equalsIndex != -1) {
109: if (delimiterIndex < keyValuePairs.length()) {
110: int equalsIndex2 = keyValuePairs.indexOf('=',
111: delimiterIndex + 1);
112: if (equalsIndex2 != -1) {
113: int delimiterIndex2 = keyValuePairs.lastIndexOf(
114: delimiter, equalsIndex2);
115: delimiterIndex = delimiterIndex2;
116: } else {
117: delimiterIndex = keyValuePairs.length();
118: }
119: }
120: String key = keyValuePairs.substring(start, equalsIndex);
121: String value = keyValuePairs.substring(equalsIndex + 1,
122: delimiterIndex);
123: put(key, value);
124: if (delimiterIndex < keyValuePairs.length()) {
125: start = delimiterIndex + 1;
126: equalsIndex = keyValuePairs.indexOf('=', start);
127: if (equalsIndex != -1) {
128: delimiterIndex = keyValuePairs.indexOf(delimiter,
129: equalsIndex);
130: if (delimiterIndex == -1)
131: delimiterIndex = keyValuePairs.length();
132: }
133: } else {
134: equalsIndex = -1;
135: }
136: }
137: }
138:
139: /**
140: * Constructor.
141: *
142: * @param keyValuePairs
143: * List of key value pairs separated by a given delimiter. For
144: * example, "param1=foo,param2=bar" where delimiter is ",".
145: * @param delimiter
146: * Delimiter string used to separate key/value pairs
147: * @param valuePattern
148: * Pattern for value. To pass a simple regular expression pass
149: * "new MetaPattern(regexp)".
150: */
151: public ValueMap(final String keyValuePairs, final String delimiter,
152: final MetaPattern valuePattern) {
153: // Get list of strings separated by the delimiter
154: final StringList pairs = StringList.tokenize(keyValuePairs,
155: delimiter);
156:
157: // Go through each string in the list
158: for (IStringIterator iterator = pairs.iterator(); iterator
159: .hasNext();) {
160: // Get the next key value pair
161: final String pair = iterator.next();
162:
163: // Parse using metapattern parser for variable assignments
164: final VariableAssignmentParser parser = new VariableAssignmentParser(
165: pair, valuePattern);
166:
167: // Does it parse?
168: if (parser.matches()) {
169: // Succeeded. Put key and value into map
170: put(parser.getKey(), parser.getValue());
171: } else {
172: throw new IllegalArgumentException(
173: "Invalid key value list: '" + keyValuePairs
174: + "'");
175: }
176: }
177: }
178:
179: /**
180: * @see java.util.Map#clear()
181: */
182: public final void clear() {
183: checkMutability();
184: super .clear();
185: }
186:
187: /**
188: * Gets a boolean value by key.
189: *
190: * @param key
191: * The key
192: * @return The value
193: * @throws StringValueConversionException
194: */
195: public final boolean getBoolean(final String key)
196: throws StringValueConversionException {
197: return getStringValue(key).toBoolean();
198: }
199:
200: /**
201: * Gets a double value by key.
202: *
203: * @param key
204: * The key
205: * @return The value
206: * @throws StringValueConversionException
207: */
208: public final double getDouble(final String key)
209: throws StringValueConversionException {
210: return getStringValue(key).toDouble();
211: }
212:
213: /**
214: * Gets a double using a default if not found.
215: *
216: * @param key
217: * The key
218: * @param defaultValue
219: * Value to use if no value in map
220: * @return The value
221: * @throws StringValueConversionException
222: */
223: public final double getDouble(final String key,
224: final double defaultValue)
225: throws StringValueConversionException {
226: return getStringValue(key).toDouble(defaultValue);
227: }
228:
229: /**
230: * Gets a duration.
231: *
232: * @param key
233: * The key
234: * @return The value
235: * @throws StringValueConversionException
236: */
237: public final Duration getDuration(final String key)
238: throws StringValueConversionException {
239: return getStringValue(key).toDuration();
240: }
241:
242: /**
243: * Gets an int.
244: *
245: * @param key
246: * The key
247: * @return The value
248: * @throws StringValueConversionException
249: */
250: public final int getInt(final String key)
251: throws StringValueConversionException {
252: return getStringValue(key).toInt();
253: }
254:
255: /**
256: * Gets an int, using a default if not found.
257: *
258: * @param key
259: * The key
260: * @param defaultValue
261: * Value to use if no value in map
262: * @return The value
263: * @throws StringValueConversionException
264: */
265: public final int getInt(final String key, final int defaultValue)
266: throws StringValueConversionException {
267: return getStringValue(key).toInt(defaultValue);
268: }
269:
270: /**
271: * Gets a long.
272: *
273: * @param key
274: * The key
275: * @return The value
276: * @throws StringValueConversionException
277: */
278: public final long getLong(final String key)
279: throws StringValueConversionException {
280: return getStringValue(key).toLong();
281: }
282:
283: /**
284: * Gets a long using a default if not found.
285: *
286: * @param key
287: * The key
288: * @param defaultValue
289: * Value to use if no value in map
290: * @return The value
291: * @throws StringValueConversionException
292: */
293: public final long getLong(final String key, final long defaultValue)
294: throws StringValueConversionException {
295: return getStringValue(key).toLong(defaultValue);
296: }
297:
298: /**
299: * Gets a string by key.
300: *
301: * @param key
302: * The get
303: * @param defaultValue
304: * Default value to return if value is null
305: * @return The string
306: */
307: public final String getString(final String key,
308: final String defaultValue) {
309: final String value = getString(key);
310: return value != null ? value : defaultValue;
311: }
312:
313: /**
314: * Gets a string by key.
315: *
316: * @param key
317: * The get
318: * @return The string
319: */
320: public final String getString(final String key) {
321: final Object o = get(key);
322: if (o == null) {
323: return null;
324: } else if (o.getClass().isArray() && Array.getLength(o) > 0) {
325: // if it is an array just get the first value
326: final Object arrayValue = Array.get(o, 0);
327: if (arrayValue == null) {
328: return null;
329: } else {
330: return arrayValue.toString();
331: }
332:
333: } else {
334: return o.toString();
335: }
336: }
337:
338: /**
339: * Gets a string by key.
340: *
341: * @param key
342: * The get
343: * @return The string
344: */
345: public final CharSequence getCharSequence(final String key) {
346: final Object o = get(key);
347: if (o == null) {
348: return null;
349: } else if (o.getClass().isArray() && Array.getLength(o) > 0) {
350: // if it is an array just get the first value
351: final Object arrayValue = Array.get(o, 0);
352: if (arrayValue == null) {
353: return null;
354: } else {
355: if (arrayValue instanceof CharSequence) {
356: return (CharSequence) arrayValue;
357: }
358: return arrayValue.toString();
359: }
360:
361: } else {
362: if (o instanceof CharSequence) {
363: return (CharSequence) o;
364: }
365: return o.toString();
366: }
367: }
368:
369: /**
370: * Gets a String array by key. If the value was a String[] it will be
371: * returned directly. If it was a String it will be converted to a String
372: * array of one. If it was an array of another type a String array will be
373: * made and the elements will be converted to a string.
374: *
375: * @param key
376: * @return The String array of that key
377: */
378: public String[] getStringArray(final String key) {
379: final Object o = get(key);
380: if (o == null) {
381: return null;
382: } else if (o instanceof String[]) {
383: return (String[]) o;
384: } else if (o.getClass().isArray()) {
385: int length = Array.getLength(o);
386: String[] array = new String[length];
387: for (int i = 0; i < length; i++) {
388: final Object arrayValue = Array.get(o, i);
389: if (arrayValue != null) {
390: array[i] = arrayValue.toString();
391: }
392: }
393: return array;
394: }
395: return new String[] { o.toString() };
396: }
397:
398: /**
399: * Gets a StringValue by key.
400: *
401: * @param key
402: * The key
403: * @return The string value object
404: */
405: public StringValue getStringValue(final String key) {
406: return StringValue.valueOf(getString(key));
407: }
408:
409: /**
410: * Gets a time.
411: *
412: * @param key
413: * The key
414: * @return The value
415: * @throws StringValueConversionException
416: */
417: public final Time getTime(final String key)
418: throws StringValueConversionException {
419: return getStringValue(key).toTime();
420: }
421:
422: /**
423: * Gets whether this value map is made immutable.
424: *
425: * @return whether this value map is made immutable
426: */
427: public final boolean isImmutable() {
428: return immutable;
429: }
430:
431: /**
432: * Makes this value map immutable by changing the underlying map
433: * representation to a collections "unmodifiableMap". After calling this
434: * method, any attempt to modify this map will result in a runtime exception
435: * being thrown by the collections classes.
436: */
437: public final void makeImmutable() {
438: this .immutable = true;
439: }
440:
441: /**
442: * @see java.util.Map#put(java.lang.Object, java.lang.Object)
443: */
444: public Object put(final Object key, final Object value) {
445: checkMutability();
446: return super .put(key, value);
447: }
448:
449: /**
450: * This methods adds the value to this map under the given key If the key
451: * already is in the map it will combine the values into a String array else
452: * it will just store the value itself
453: *
454: * @param key
455: * The key to store the value under.
456: * @param value
457: * The value that must be added/merged to the map
458: * @return The value itself if there was no previous value or a string array
459: * with the combined values.
460: */
461: public final Object add(final String key, final String value) {
462: checkMutability();
463: final Object o = get(key);
464: if (o == null) {
465: return put(key, value);
466: } else if (o.getClass().isArray()) {
467: int length = Array.getLength(o);
468: String destArray[] = new String[length + 1];
469: for (int i = 0; i < length; i++) {
470: final Object arrayValue = Array.get(o, i);
471: if (arrayValue != null) {
472: destArray[i] = arrayValue.toString();
473: }
474: }
475: destArray[length] = value;
476:
477: return put(key, destArray);
478: } else {
479: return put(key, new String[] { o.toString(), value });
480: }
481: }
482:
483: /**
484: * @see java.util.Map#putAll(java.util.Map)
485: */
486: public void putAll(final Map map) {
487: checkMutability();
488: super .putAll(map);
489: }
490:
491: /**
492: * @see java.util.Map#remove(java.lang.Object)
493: */
494: public Object remove(final Object key) {
495: checkMutability();
496: return super .remove(key);
497: }
498:
499: /**
500: * Provided the hash key is a string and you need to access the value
501: * ignoring ignoring the keys case (upper or lower letter), than you may use
502: * this method to get the correct writing.
503: *
504: * @param key
505: * @return The key with the correct writing
506: */
507: public String getKey(final String key) {
508: Iterator iter = this .keySet().iterator();
509: while (iter.hasNext()) {
510: Object keyValue = iter.next();
511: if (keyValue instanceof String) {
512: String keyString = (String) keyValue;
513: if (key.equalsIgnoreCase(keyString)) {
514: return keyString;
515: }
516: }
517: }
518: return null;
519: }
520:
521: /**
522: * Gets a string representation of this object
523: *
524: * @return String representation of map consistent with tag attribute style
525: * of markup elements, for example: a="x" b="y" c="z"
526: */
527: public String toString() {
528: final StringBuffer buffer = new StringBuffer();
529: for (final Iterator iterator = entrySet().iterator(); iterator
530: .hasNext();) {
531: final Map.Entry entry = (Map.Entry) iterator.next();
532: buffer.append(entry.getKey());
533: buffer.append(" = \"");
534: final Object value = entry.getValue();
535: if (value == null) {
536: buffer.append("null");
537: } else if (value.getClass().isArray()) {
538: buffer.append(Arrays.asList((Object[]) value));
539: } else {
540: buffer.append(value);
541: }
542:
543: buffer.append("\"");
544: if (iterator.hasNext()) {
545: buffer.append(' ');
546: }
547: }
548: return buffer.toString();
549: }
550:
551: /**
552: * Throw exception if map is immutable.
553: */
554: private final void checkMutability() {
555: if (immutable) {
556: throw new UnsupportedOperationException("Map is immutable");
557: }
558: }
559: }
|