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 */
017package org.apache.wicket.util.value;
018
019import java.lang.reflect.Array;
020import java.lang.reflect.InvocationTargetException;
021import java.lang.reflect.Method;
022import java.sql.Time;
023import java.time.Duration;
024import java.time.Instant;
025import java.util.Arrays;
026import java.util.LinkedHashMap;
027import java.util.Map;
028
029import org.apache.wicket.util.parse.metapattern.MetaPattern;
030import org.apache.wicket.util.parse.metapattern.parsers.VariableAssignmentParser;
031import org.apache.wicket.util.string.AppendingStringBuffer;
032import org.apache.wicket.util.string.IStringIterator;
033import org.apache.wicket.util.string.StringList;
034import org.apache.wicket.util.string.StringValue;
035import org.apache.wicket.util.string.StringValueConversionException;
036
037
038/**
039 * A <code>IValueMap</code> implementation that holds values, parses <code>String</code>s, and
040 * exposes a variety of convenience methods.
041 * <p>
042 * In addition to a no-arg constructor and a copy constructor that takes a <code>Map</code>
043 * argument, <code>ValueMap</code>s can be constructed using a parsing constructor.
044 * <code>ValueMap(String)</code> will parse values from the string in comma separated key/value
045 * assignment pairs. For example, <code>new ValueMap("a=9,b=foo")</code>.
046 * <p>
047 * Values can be retrieved from the <code>ValueMap</code> in the usual way or with methods that do
048 * handy conversions to various types, including <code>String</code>, <code>StringValue</code>,
049 * <code>int</code>, <code>long</code>, <code>double</code>, <code>Time</code> and
050 * <code>Duration</code>.
051 * <p>
052 * The <code>makeImmutable</code> method will make the underlying <code>Map</code> immutable.
053 * Further attempts to change the <code>Map</code> will result in a <code>RuntimeException</code>.
054 * <p>
055 * The <code>toString</code> method converts a <code>ValueMap</code> object to a readable key/value
056 * string for diagnostics.
057 * 
058 * @author Jonathan Locke
059 * @author Doug Donohoe
060 * @since 1.2.6
061 */
062public class ValueMap extends LinkedHashMap<String, Object> implements IValueMap
063{
064        /** an empty <code>ValueMap</code>. */
065        public static final ValueMap EMPTY_MAP;
066
067        /** create EMPTY_MAP, make immutable * */
068        static
069        {
070                EMPTY_MAP = new ValueMap();
071                EMPTY_MAP.makeImmutable();
072        }
073
074        private static final long serialVersionUID = 1L;
075
076        /**
077         * <code>true</code> if this <code>ValueMap</code> has been made immutable.
078         */
079        private boolean immutable = false;
080
081        /**
082         * Constructs empty <code>ValueMap</code>.
083         */
084        public ValueMap()
085        {
086                super();
087        }
088
089        /**
090         * Copy constructor.
091         * 
092         * @param map
093         *            the <code>ValueMap</code> to copy
094         */
095        public ValueMap(final Map<? extends String, ?> map)
096        {
097                super();
098
099                super.putAll(map);
100        }
101
102        /**
103         * Constructor.
104         * <p>
105         * NOTE: Please use <code>RequestUtils.decodeParameters()</code> if you wish to properly decode
106         * a request URL.
107         * 
108         * @param keyValuePairs
109         *            list of key/value pairs separated by commas. For example, "
110         *            <code>param1=foo,param2=bar</code>"
111         */
112        public ValueMap(final String keyValuePairs)
113        {
114                this(keyValuePairs, ",");
115        }
116
117        /**
118         * Constructor.
119         * <p>
120         * NOTE: Please use <code>RequestUtils.decodeParameters()</code> if you wish to properly decode
121         * a request URL.
122         * 
123         * @param keyValuePairs
124         *            list of key/value pairs separated by a given delimiter. For example, "
125         *            <code>param1=foo,param2=bar</code>" where delimiter is "<code>,</code>".
126         * @param delimiter
127         *            delimiter <code>String</code> used to separate key/value pairs
128         */
129        public ValueMap(final String keyValuePairs, final String delimiter)
130        {
131                super();
132
133                int start = 0;
134                int equalsIndex = keyValuePairs.indexOf('=');
135                int delimiterIndex = keyValuePairs.indexOf(delimiter, equalsIndex);
136                if (delimiterIndex == -1)
137                {
138                        delimiterIndex = keyValuePairs.length();
139                }
140                while (equalsIndex != -1)
141                {
142                        if (delimiterIndex < keyValuePairs.length())
143                        {
144                                int equalsIndex2 = keyValuePairs.indexOf('=', delimiterIndex + 1);
145                                if (equalsIndex2 != -1)
146                                {
147                                        delimiterIndex = keyValuePairs.lastIndexOf(delimiter, equalsIndex2);
148                                }
149                                else
150                                {
151                                        delimiterIndex = keyValuePairs.length();
152                                }
153                        }
154                        String key = keyValuePairs.substring(start, equalsIndex);
155                        String value = keyValuePairs.substring(equalsIndex + 1, delimiterIndex);
156                        add(key, value);
157                        if (delimiterIndex < keyValuePairs.length())
158                        {
159                                start = delimiterIndex + 1;
160                                equalsIndex = keyValuePairs.indexOf('=', start);
161                                if (equalsIndex != -1)
162                                {
163                                        delimiterIndex = keyValuePairs.indexOf(delimiter, equalsIndex);
164                                        if (delimiterIndex == -1)
165                                        {
166                                                delimiterIndex = keyValuePairs.length();
167                                        }
168                                }
169                        }
170                        else
171                        {
172                                equalsIndex = -1;
173                        }
174                }
175        }
176
177        /**
178         * Constructor.
179         * 
180         * @param keyValuePairs
181         *            list of key/value pairs separated by a given delimiter. For example, "
182         *            <code>param1=foo,param2=bar</code>" where delimiter is "<code>,</code>".
183         * @param delimiter
184         *            delimiter string used to separate key/value pairs
185         * @param valuePattern
186         *            pattern for value. To pass a simple regular expression, pass "
187         *            <code>new MetaPattern(regexp)</code>".
188         */
189        public ValueMap(final String keyValuePairs, final String delimiter,
190                final MetaPattern valuePattern)
191        {
192                super();
193
194                // Get list of strings separated by the delimiter
195                final StringList pairs = StringList.tokenize(keyValuePairs, delimiter);
196
197                // Go through each string in the list
198                for (IStringIterator iterator = pairs.iterator(); iterator.hasNext();)
199                {
200                        // Get the next key value pair
201                        final String pair = iterator.next();
202
203                        // Parse using metapattern parser for variable assignments
204                        final VariableAssignmentParser parser = new VariableAssignmentParser(pair, valuePattern);
205
206                        // Does it parse?
207                        if (parser.matches())
208                        {
209                                // Succeeded. Put key and value into map
210                                put(parser.getKey(), parser.getValue());
211                        }
212                        else
213                        {
214                                throw new IllegalArgumentException("Invalid key value list: '" + keyValuePairs +
215                                        '\'');
216                        }
217                }
218        }
219
220        @Override
221        public final void clear()
222        {
223                checkMutability();
224                super.clear();
225        }
226
227        @Override
228        public final boolean getBoolean(final String key) throws StringValueConversionException
229        {
230                return getStringValue(key).toBoolean();
231        }
232
233        @Override
234        public final double getDouble(final String key) throws StringValueConversionException
235        {
236                return getStringValue(key).toDouble();
237        }
238
239        @Override
240        public final double getDouble(final String key, final double defaultValue)
241        {
242                return getStringValue(key).toDouble(defaultValue);
243        }
244
245        @Override
246        public final Duration getDuration(final String key) throws StringValueConversionException
247        {
248                return getStringValue(key).toDuration();
249        }
250
251        @Override
252        public final int getInt(final String key) throws StringValueConversionException
253        {
254                return getStringValue(key).toInt();
255        }
256
257        @Override
258        public final int getInt(final String key, final int defaultValue)
259        {
260                return getStringValue(key).toInt(defaultValue);
261        }
262
263        @Override
264        public final long getLong(final String key) throws StringValueConversionException
265        {
266                return getStringValue(key).toLong();
267        }
268
269        @Override
270        public final long getLong(final String key, final long defaultValue)
271        {
272                return getStringValue(key).toLong(defaultValue);
273        }
274
275        @Override
276        public final String getString(final String key, final String defaultValue)
277        {
278                final String value = getString(key);
279                return value != null ? value : defaultValue;
280        }
281
282        @Override
283        public final String getString(final String key)
284        {
285                final Object o = get(key);
286                if (o == null)
287                {
288                        return null;
289                }
290                else if (o.getClass().isArray() && (Array.getLength(o) > 0))
291                {
292                        // if it is an array just get the first value
293                        final Object arrayValue = Array.get(o, 0);
294                        if (arrayValue == null)
295                        {
296                                return null;
297                        }
298                        else
299                        {
300                                return arrayValue.toString();
301                        }
302
303                }
304                else
305                {
306                        return o.toString();
307                }
308        }
309
310        @Override
311        public final CharSequence getCharSequence(final String key)
312        {
313                final Object o = get(key);
314                if (o == null)
315                {
316                        return null;
317                }
318                else if (o.getClass().isArray() && (Array.getLength(o) > 0))
319                {
320                        // if it is an array just get the first value
321                        final Object arrayValue = Array.get(o, 0);
322                        if (arrayValue == null)
323                        {
324                                return null;
325                        }
326                        else
327                        {
328                                if (arrayValue instanceof CharSequence)
329                                {
330                                        return (CharSequence)arrayValue;
331                                }
332                                return arrayValue.toString();
333                        }
334
335                }
336                else
337                {
338                        if (o instanceof CharSequence)
339                        {
340                                return (CharSequence)o;
341                        }
342                        return o.toString();
343                }
344        }
345
346        @Override
347        public String[] getStringArray(final String key)
348        {
349                final Object o = get(key);
350                if (o == null)
351                {
352                        return null;
353                }
354                else if (o instanceof String[])
355                {
356                        return (String[])o;
357                }
358                else if (o.getClass().isArray())
359                {
360                        int length = Array.getLength(o);
361                        String[] array = new String[length];
362                        for (int i = 0; i < length; i++)
363                        {
364                                final Object arrayValue = Array.get(o, i);
365                                if (arrayValue != null)
366                                {
367                                        array[i] = arrayValue.toString();
368                                }
369                        }
370                        return array;
371                }
372                return new String[] { o.toString() };
373        }
374
375        @Override
376        public StringValue getStringValue(final String key)
377        {
378                return StringValue.valueOf(getString(key));
379        }
380
381        @Override
382        public final Instant getInstant(final String key) throws StringValueConversionException
383        {
384                return getStringValue(key).toInstant();
385        }
386
387        @Override
388        public final boolean isImmutable()
389        {
390                return immutable;
391        }
392
393        @Override
394        public final IValueMap makeImmutable()
395        {
396                immutable = true;
397                return this;
398        }
399
400        @Override
401        public Object put(final String key, final Object value)
402        {
403                checkMutability();
404                return super.put(key, value);
405        }
406
407        /**
408         * Adds the value to this <code>ValueMap</code> with the given key. If the key already is in the
409         * <code>ValueMap</code> it will combine the values into a <code>String</code> array, else it
410         * will just store the value itself.
411         * 
412         * @param key
413         *            the key to store the value under
414         * @param value
415         *            the value that must be added/merged to the <code>ValueMap</code>
416         * @return the value itself if there was no previous value, or a <code>String</code> array with
417         *         the combined values
418         */
419        public final Object add(final String key, final String value)
420        {
421                checkMutability();
422                final Object o = get(key);
423                if (o == null)
424                {
425                        return put(key, value);
426                }
427                else if (o.getClass().isArray())
428                {
429                        int length = Array.getLength(o);
430                        String destArray[] = new String[length + 1];
431                        for (int i = 0; i < length; i++)
432                        {
433                                final Object arrayValue = Array.get(o, i);
434                                if (arrayValue != null)
435                                {
436                                        destArray[i] = arrayValue.toString();
437                                }
438                        }
439                        destArray[length] = value;
440
441                        return put(key, destArray);
442                }
443                else
444                {
445                        return put(key, new String[] { o.toString(), value });
446                }
447        }
448
449        @Override
450        public void putAll(final Map<? extends String, ?> map)
451        {
452                checkMutability();
453                super.putAll(map);
454        }
455
456        @Override
457        public Object remove(final Object key)
458        {
459                checkMutability();
460                return super.remove(key);
461        }
462
463        @Override
464        public String getKey(final String key)
465        {
466                for (String other : keySet())
467                {
468                        if (other.equalsIgnoreCase(key))
469                        {
470                                return other;
471                        }
472                }
473                return null;
474        }
475
476        /**
477         * Generates a <code>String</code> representation of this object.
478         * 
479         * @return <code>String</code> representation of this <code>ValueMap</code> consistent with the
480         *         tag-attribute style of markup elements. For example: <code>a="x" b="y" c="z"</code>.
481         */
482        @Override
483        public String toString()
484        {
485                final AppendingStringBuffer buffer = new AppendingStringBuffer();
486                boolean first = true;
487                for (Map.Entry<String, Object> entry : entrySet())
488                {
489                        if (first == false)
490                        {
491                                buffer.append(' ');
492                        }
493                        first = false;
494
495                        buffer.append(entry.getKey());
496                        buffer.append(" = \"");
497                        final Object value = entry.getValue();
498                        if (value == null)
499                        {
500                                buffer.append("null");
501                        }
502                        else if (value.getClass().isArray())
503                        {
504                                buffer.append(Arrays.asList((Object[])value));
505                        }
506                        else
507                        {
508                                buffer.append(value);
509                        }
510
511                        buffer.append('\"');
512                }
513                return buffer.toString();
514        }
515
516        /**
517         * Throws an exception if <code>ValueMap</code> is immutable.
518         */
519        private void checkMutability()
520        {
521                if (immutable)
522                {
523                        throw new UnsupportedOperationException("Map is immutable");
524                }
525        }
526
527        // //
528        // // getAs convenience methods
529        // //
530
531        @Override
532        public Boolean getAsBoolean(final String key)
533        {
534                if (!containsKey(key))
535                {
536                        return null;
537                }
538
539                try
540                {
541                        return getBoolean(key);
542                }
543                catch (StringValueConversionException ignored)
544                {
545                        return null;
546                }
547        }
548
549        @Override
550        public boolean getAsBoolean(final String key, final boolean defaultValue)
551        {
552                if (!containsKey(key))
553                {
554                        return defaultValue;
555                }
556
557                try
558                {
559                        return getBoolean(key);
560                }
561                catch (StringValueConversionException ignored)
562                {
563                        return defaultValue;
564                }
565        }
566
567        @Override
568        public Integer getAsInteger(final String key)
569        {
570                if (!containsKey(key))
571                {
572                        return null;
573                }
574
575                try
576                {
577                        return getInt(key);
578                }
579                catch (StringValueConversionException ignored)
580                {
581                        return null;
582                }
583        }
584
585        @Override
586        public int getAsInteger(final String key, final int defaultValue)
587        {
588                return getInt(key, defaultValue);
589        }
590
591        @Override
592        public Long getAsLong(final String key)
593        {
594                if (!containsKey(key))
595                {
596                        return null;
597                }
598
599                try
600                {
601                        return getLong(key);
602                }
603                catch (StringValueConversionException ignored)
604                {
605                        return null;
606                }
607        }
608
609        @Override
610        public long getAsLong(final String key, final long defaultValue)
611        {
612                return getLong(key, defaultValue);
613        }
614
615        @Override
616        public Double getAsDouble(final String key)
617        {
618                if (!containsKey(key))
619                {
620                        return null;
621                }
622
623                try
624                {
625                        return getDouble(key);
626                }
627                catch (StringValueConversionException ignored)
628                {
629                        return null;
630                }
631        }
632
633        @Override
634        public double getAsDouble(final String key, final double defaultValue)
635        {
636                return getDouble(key, defaultValue);
637        }
638
639        @Override
640        public Duration getAsDuration(final String key)
641        {
642                return getAsDuration(key, null);
643        }
644
645        @Override
646        public Duration getAsDuration(final String key, final Duration defaultValue)
647        {
648                if (!containsKey(key))
649                {
650                        return defaultValue;
651                }
652
653                try
654                {
655                        return getDuration(key);
656                }
657                catch (StringValueConversionException ignored)
658                {
659                        return defaultValue;
660                }
661        }
662
663        @Override
664        public Instant getAsInstant(final String key)
665        {
666                return getAsTime(key, null);
667        }
668
669        @Override
670        public Instant getAsTime(final String key, final Instant defaultValue)
671        {
672                if (!containsKey(key))
673                {
674                        return defaultValue;
675                }
676
677                try
678                {
679                        return getInstant(key);
680                }
681                catch (StringValueConversionException ignored)
682                {
683                        return defaultValue;
684                }
685        }
686
687        @Override
688        public <T extends Enum<T>> T getAsEnum(final String key, final Class<T> eClass)
689        {
690                // explicitly pass T as type to be able to build with JDK 1.8. WICKET-5427
691                return this.getEnumImpl(key, eClass, (T)null);
692        }
693
694        @Override
695        public <T extends Enum<T>> T getAsEnum(final String key, final T defaultValue)
696        {
697                if (defaultValue == null)
698                {
699                        throw new IllegalArgumentException("Default value cannot be null");
700                }
701
702                return getEnumImpl(key, defaultValue.getClass(), defaultValue);
703        }
704
705        @Override
706        public <T extends Enum<T>> T getAsEnum(final String key, final Class<T> eClass,
707                final T defaultValue)
708        {
709                return getEnumImpl(key, eClass, defaultValue);
710        }
711
712        /**
713         * get enum implementation
714         * 
715         * @param key
716         * @param eClass
717         * @param defaultValue
718         * @param <T>
719         * @return Enum
720         */
721        @SuppressWarnings({ "unchecked" })
722        private <T extends Enum<T>> T getEnumImpl(final String key, final Class<?> eClass,
723                final T defaultValue)
724        {
725                if (eClass == null)
726                {
727                        throw new IllegalArgumentException("eClass value cannot be null");
728                }
729
730                String value = getString(key);
731                if (value == null)
732                {
733                        return defaultValue;
734                }
735
736                Method valueOf = null;
737                try
738                {
739                        valueOf = eClass.getMethod("valueOf", String.class);
740                }
741                catch (NoSuchMethodException e)
742                {
743                        throw new RuntimeException("Could not find method valueOf(String s) for " +
744                                eClass.getName(), e);
745                }
746
747                try
748                {
749                        return (T)valueOf.invoke(eClass, value);
750                }
751                catch (IllegalAccessException e)
752                {
753                        throw new RuntimeException("Could not invoke method valueOf(String s) on " +
754                                eClass.getName(), e);
755                }
756                catch (InvocationTargetException e)
757                {
758                        // IllegalArgumentException thrown if enum isn't defined - just return default
759                        if (e.getCause() instanceof IllegalArgumentException)
760                        {
761                                return defaultValue;
762                        }
763                        throw new RuntimeException(e); // shouldn't happen
764                }
765        }
766}