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