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}