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.string; 018 019import java.text.DecimalFormat; 020import java.text.DecimalFormatSymbols; 021import java.text.NumberFormat; 022import java.text.ParseException; 023import java.time.Duration; 024import java.time.Instant; 025import java.time.format.DateTimeParseException; 026import java.util.Locale; 027 028import org.apache.wicket.util.io.IClusterable; 029import org.apache.wicket.util.lang.Args; 030import org.apache.wicket.util.lang.Objects; 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033 034 035/** 036 * Holds an immutable String value and optionally a Locale, with methods to convert to various 037 * types. Also provides some handy parsing methods and a variety of static factory methods. 038 * <p> 039 * Objects can be constructed directly from Strings or by using the valueOf() static factory 040 * methods. The repeat() static factory methods provide a way of generating a String value that 041 * repeats a given char or String a number of times. 042 * <p> 043 * Conversions to a wide variety of types can be found in the to*() methods. A generic conversion 044 * can be achieved with to(Class). 045 * <P> 046 * The beforeFirst(), afterFirst(), beforeLast() and afterLast() methods are handy for parsing 047 * things like paths and filenames. 048 * 049 * @author Jonathan Locke 050 */ 051public class StringValue implements IClusterable 052{ 053 private static final long serialVersionUID = 1L; 054 055 private static final Logger LOG = LoggerFactory.getLogger(StringValue.class); 056 057 /** Locale to be used for formatting and parsing. */ 058 private final Locale locale; 059 060 /** The underlying string. */ 061 private final String text; 062 063 /** 064 * @param times 065 * Number of times to repeat character 066 * @param c 067 * Character to repeat 068 * @return Repeated character string 069 */ 070 public static StringValue repeat(final int times, final char c) 071 { 072 final AppendingStringBuffer buffer = new AppendingStringBuffer(times); 073 074 for (int i = 0; i < times; i++) 075 { 076 buffer.append(c); 077 } 078 079 return valueOf(buffer); 080 } 081 082 /** 083 * @param times 084 * Number of times to repeat string 085 * @param s 086 * String to repeat 087 * @return Repeated character string 088 */ 089 public static StringValue repeat(final int times, final String s) 090 { 091 final AppendingStringBuffer buffer = new AppendingStringBuffer(times); 092 093 for (int i = 0; i < times; i++) 094 { 095 buffer.append(s); 096 } 097 098 return valueOf(buffer); 099 } 100 101 /** 102 * Converts the given input to an instance of StringValue. 103 * 104 * @param value 105 * Double precision value 106 * @return String value formatted with one place after decimal 107 */ 108 public static StringValue valueOf(final double value) 109 { 110 return valueOf(value, Locale.getDefault(Locale.Category.FORMAT)); 111 } 112 113 /** 114 * Converts the given input to an instance of StringValue. 115 * 116 * @param value 117 * Double precision value 118 * @param places 119 * Number of places after decimal 120 * @param locale 121 * Locale to be used for formatting 122 * @return String value formatted with the given number of places after decimal 123 */ 124 public static StringValue valueOf(final double value, final int places, final Locale locale) 125 { 126 if (Double.isNaN(value) || Double.isInfinite(value)) 127 { 128 return valueOf("N/A"); 129 } 130 else 131 { 132 final DecimalFormat format = new DecimalFormat("#." + repeat(places, '#'), 133 new DecimalFormatSymbols(locale)); 134 return valueOf(format.format(value)); 135 } 136 } 137 138 /** 139 * Converts the given input to an instance of StringValue. 140 * 141 * @param value 142 * Double precision value 143 * @param locale 144 * Locale to be used for formatting 145 * @return String value formatted with one place after decimal 146 */ 147 public static StringValue valueOf(final double value, final Locale locale) 148 { 149 return valueOf(value, 1, locale); 150 } 151 152 /** 153 * Converts the given input to an instance of StringValue. 154 * 155 * @param object 156 * An object 157 * @return String value for object 158 */ 159 public static StringValue valueOf(final Object object) 160 { 161 return valueOf(Strings.toString(object)); 162 } 163 164 /** 165 * Converts the given input to an instance of StringValue. 166 * 167 * @param object 168 * An object 169 * @param locale 170 * Locale to be used for formatting 171 * @return String value for object 172 */ 173 public static StringValue valueOf(final Object object, final Locale locale) 174 { 175 return valueOf(Strings.toString(object), locale); 176 } 177 178 /** 179 * Converts the given input to an instance of StringValue. 180 * 181 * @param string 182 * A string 183 * @return String value for string 184 */ 185 public static StringValue valueOf(final String string) 186 { 187 return new StringValue(string); 188 } 189 190 /** 191 * Converts the given input to an instance of StringValue. 192 * 193 * @param string 194 * A string 195 * @param locale 196 * Locale to be used for formatting 197 * @return String value for string 198 */ 199 public static StringValue valueOf(final String string, final Locale locale) 200 { 201 return new StringValue(string, locale); 202 } 203 204 /** 205 * Converts the given input to an instance of StringValue. 206 * 207 * @param buffer 208 * A string buffer 209 * @return String value 210 */ 211 public static StringValue valueOf(final AppendingStringBuffer buffer) 212 { 213 return valueOf(buffer.toString()); 214 } 215 216 /** 217 * Private constructor to force use of static factory methods. 218 * 219 * @param text 220 * The text for this string value 221 */ 222 protected StringValue(final String text) 223 { 224 this(text, Locale.getDefault()); 225 } 226 227 /** 228 * Private constructor to force use of static factory methods. 229 * 230 * @param text 231 * The text for this string value 232 * @param locale 233 * the locale for formatting and parsing 234 */ 235 protected StringValue(final String text, final Locale locale) 236 { 237 this.text = text; 238 this.locale = locale; 239 } 240 241 /** 242 * Gets the substring after the first occurrence given char. 243 * 244 * @param c 245 * char to scan for 246 * @return the substring 247 */ 248 public final String afterFirst(final char c) 249 { 250 return Strings.afterFirst(text, c); 251 } 252 253 /** 254 * Gets the substring after the last occurrence given char. 255 * 256 * @param c 257 * char to scan for 258 * @return the substring 259 */ 260 public final String afterLast(final char c) 261 { 262 return Strings.afterLast(text, c); 263 } 264 265 /** 266 * Gets the substring before the first occurrence given char. 267 * 268 * @param c 269 * char to scan for 270 * @return the substring 271 */ 272 public final String beforeFirst(final char c) 273 { 274 return Strings.beforeFirst(text, c); 275 } 276 277 /** 278 * Gets the substring before the last occurrence given char. 279 * 280 * @param c 281 * char to scan for 282 * @return the substring 283 */ 284 public final String beforeLast(final char c) 285 { 286 return Strings.afterLast(text, c); 287 } 288 289 /** 290 * Replaces on this text. 291 * 292 * @param searchFor 293 * What to search for 294 * @param replaceWith 295 * What to replace with 296 * @return This string value with searchFor replaces with replaceWith 297 */ 298 public final CharSequence replaceAll(final CharSequence searchFor, 299 final CharSequence replaceWith) 300 { 301 return Strings.replaceAll(text, searchFor, replaceWith); 302 } 303 304 /** 305 * Converts this StringValue to a given type. 306 * 307 * @param type 308 * The type to convert to 309 * @return The converted value 310 * @throws StringValueConversionException 311 */ 312 @SuppressWarnings({ "unchecked", "rawtypes" }) 313 public final <T> T to(final Class<T> type) throws StringValueConversionException 314 { 315 if (type == null) 316 { 317 return null; 318 } 319 320 if (type == String.class) 321 { 322 return (T)toString(); 323 } 324 325 if ((type == Integer.TYPE) || (type == Integer.class)) 326 { 327 return (T)toInteger(); 328 } 329 330 if ((type == Long.TYPE) || (type == Long.class)) 331 { 332 return (T)toLongObject(); 333 } 334 335 if ((type == Boolean.TYPE) || (type == Boolean.class)) 336 { 337 return (T)toBooleanObject(); 338 } 339 340 if ((type == Double.TYPE) || (type == Double.class)) 341 { 342 return (T)toDoubleObject(); 343 } 344 345 if ((type == Character.TYPE) || (type == Character.class)) 346 { 347 return (T)toCharacter(); 348 } 349 350 if (type == Instant.class) 351 { 352 return (T)toInstant(); 353 } 354 355 if (type == Duration.class) 356 { 357 return (T)toDuration(); 358 } 359 360 if (type.isEnum()) 361 { 362 return (T)toEnum((Class)type); 363 } 364 365 throw new StringValueConversionException( 366 "Cannot convert '" + toString() + "'to type " + type); 367 } 368 369 /** 370 * Converts this StringValue to a given type or {@code null} if the value is empty. 371 * 372 * @param type 373 * The type to convert to 374 * @return The converted value 375 * @throws StringValueConversionException 376 */ 377 public final <T> T toOptional(final Class<T> type) throws StringValueConversionException 378 { 379 return Strings.isEmpty(text) ? null : to(type); 380 } 381 382 /** 383 * Convert this text to a boolean. 384 * 385 * @return This string value as a boolean 386 * @throws StringValueConversionException 387 */ 388 public final boolean toBoolean() throws StringValueConversionException 389 { 390 return Strings.isTrue(text); 391 } 392 393 /** 394 * Convert to boolean, returning default value if text is inconvertible. 395 * 396 * @param defaultValue 397 * the default value 398 * @return the converted text as a boolean or the default value if text is empty or 399 * inconvertible 400 * @see Strings#isTrue(String) 401 */ 402 public final boolean toBoolean(final boolean defaultValue) 403 { 404 if (text != null) 405 { 406 try 407 { 408 return toBoolean(); 409 } 410 catch (StringValueConversionException x) 411 { 412 if (LOG.isDebugEnabled()) 413 { 414 LOG.debug( 415 String.format("An error occurred while converting '%s' to a boolean: %s", 416 text, x.getMessage()), 417 x); 418 } 419 } 420 } 421 return defaultValue; 422 } 423 424 /** 425 * Convert this text to a boolean. 426 * 427 * @return Converted text 428 * @throws StringValueConversionException 429 */ 430 public final Boolean toBooleanObject() throws StringValueConversionException 431 { 432 return Strings.toBoolean(text); 433 } 434 435 /** 436 * Convert this text to a char. 437 * 438 * @return This string value as a character 439 * @throws StringValueConversionException 440 */ 441 public final char toChar() throws StringValueConversionException 442 { 443 return Strings.toChar(text); 444 } 445 446 /** 447 * Convert to character, returning default value if text is inconvertible. 448 * 449 * @param defaultValue 450 * the default value 451 * @return the converted text as a primitive char or the default value if text is not a single 452 * character 453 */ 454 public final char toChar(final char defaultValue) 455 { 456 if (text != null) 457 { 458 try 459 { 460 return toChar(); 461 } 462 catch (StringValueConversionException x) 463 { 464 if (LOG.isDebugEnabled()) 465 { 466 LOG.debug( 467 String.format("An error occurred while converting '%s' to a character: %s", 468 text, x.getMessage()), 469 x); 470 } 471 } 472 } 473 return defaultValue; 474 } 475 476 /** 477 * Convert this text to a Character. 478 * 479 * @return Converted text 480 * @throws StringValueConversionException 481 */ 482 public final Character toCharacter() throws StringValueConversionException 483 { 484 return toChar(); 485 } 486 487 /** 488 * Convert this text to a double. 489 * 490 * @return Converted text 491 * @throws StringValueConversionException 492 */ 493 public final double toDouble() throws StringValueConversionException 494 { 495 try 496 { 497 return NumberFormat.getNumberInstance(locale).parse(text).doubleValue(); 498 } 499 catch (ParseException e) 500 { 501 throw new StringValueConversionException( 502 "Unable to convert '" + text + "' to a double value", e); 503 } 504 } 505 506 /** 507 * Convert to double, returning default value if text is inconvertible. 508 * 509 * @param defaultValue 510 * the default value 511 * @return the converted text as a double or the default value if text is empty or inconvertible 512 */ 513 public final double toDouble(final double defaultValue) 514 { 515 if (text != null) 516 { 517 try 518 { 519 return toDouble(); 520 } 521 catch (Exception x) 522 { 523 if (LOG.isDebugEnabled()) 524 { 525 LOG.debug( 526 String.format("An error occurred while converting '%s' to a double: %s", 527 text, x.getMessage()), 528 x); 529 } 530 } 531 } 532 return defaultValue; 533 } 534 535 /** 536 * Convert this text to a Double. 537 * 538 * @return Converted text 539 * @throws StringValueConversionException 540 */ 541 public final Double toDoubleObject() throws StringValueConversionException 542 { 543 return toDouble(); 544 } 545 546 /** 547 * Convert this text to a Duration instance. 548 * 549 * @return Converted text 550 * @throws StringValueConversionException 551 * @see Duration#parse(CharSequence) 552 */ 553 public final Duration toDuration() throws StringValueConversionException 554 { 555 try 556 { 557 return Duration.parse(text); 558 } 559 catch (Exception e) 560 { 561 throw new StringValueConversionException("Unable to convert '" + text + "' to a Duration value", e); 562 } 563 } 564 565 /** 566 * Convert to duration, returning default value if text is inconvertible. 567 * 568 * @param defaultValue 569 * the default value 570 * @return the converted text as a duration or the default value if text is empty or 571 * inconvertible 572 * @see Duration#parse(CharSequence) 573 */ 574 public final Duration toDuration(final Duration defaultValue) 575 { 576 if (text != null) 577 { 578 try 579 { 580 return toDuration(); 581 } 582 catch (Exception x) 583 { 584 if (LOG.isDebugEnabled()) 585 { 586 LOG.debug( 587 String.format("An error occurred while converting '%s' to a Duration: %s", 588 text, x.getMessage()), 589 x); 590 } 591 } 592 } 593 return defaultValue; 594 } 595 596 /** 597 * Convert this text to an int. 598 * 599 * @return Converted text 600 * @throws StringValueConversionException 601 */ 602 public final int toInt() throws StringValueConversionException 603 { 604 try 605 { 606 return Integer.parseInt(text); 607 } 608 catch (NumberFormatException e) 609 { 610 throw new StringValueConversionException( 611 "Unable to convert '" + text + "' to an int value", e); 612 } 613 } 614 615 /** 616 * Convert to integer, returning default value if text is inconvertible. 617 * 618 * @param defaultValue 619 * the default value 620 * @return the converted text as an integer or the default value if text is not an integer 621 */ 622 public final int toInt(final int defaultValue) 623 { 624 if (text != null) 625 { 626 try 627 { 628 return toInt(); 629 } 630 catch (StringValueConversionException x) 631 { 632 if (LOG.isDebugEnabled()) 633 { 634 LOG.debug( 635 String.format("An error occurred while converting '%s' to an integer: %s", 636 text, x.getMessage()), 637 x); 638 } 639 } 640 } 641 return defaultValue; 642 } 643 644 /** 645 * Convert this text to an Integer. 646 * 647 * @return Converted text 648 * @throws StringValueConversionException 649 */ 650 public final Integer toInteger() throws StringValueConversionException 651 { 652 try 653 { 654 return Integer.parseInt(text, 10); 655 } 656 catch (NumberFormatException e) 657 { 658 throw new StringValueConversionException( 659 "Unable to convert '" + text + "' to an Integer value", e); 660 } 661 } 662 663 /** 664 * Convert this text to a long. 665 * 666 * @return Converted text 667 * @throws StringValueConversionException 668 */ 669 public final long toLong() throws StringValueConversionException 670 { 671 try 672 { 673 return Long.parseLong(text); 674 } 675 catch (NumberFormatException e) 676 { 677 throw new StringValueConversionException( 678 "Unable to convert '" + text + "' to a long value", e); 679 } 680 } 681 682 /** 683 * Convert to long integer, returning default value if text is inconvertible. 684 * 685 * @param defaultValue 686 * the default value 687 * @return the converted text as a long integer or the default value if text is empty or 688 * inconvertible 689 */ 690 public final long toLong(final long defaultValue) 691 { 692 if (text != null) 693 { 694 try 695 { 696 return toLong(); 697 } 698 catch (StringValueConversionException x) 699 { 700 if (LOG.isDebugEnabled()) 701 { 702 LOG.debug(String.format("An error occurred while converting '%s' to a long: %s", 703 text, x.getMessage()), x); 704 } 705 } 706 } 707 return defaultValue; 708 } 709 710 /** 711 * Convert this text to a Long. 712 * 713 * @return Converted text 714 * @throws StringValueConversionException 715 */ 716 public final Long toLongObject() throws StringValueConversionException 717 { 718 try 719 { 720 return Long.parseLong(text, 10); 721 } 722 catch (NumberFormatException e) 723 { 724 throw new StringValueConversionException( 725 "Unable to convert '" + text + "' to a Long value", e); 726 } 727 } 728 729 /** 730 * Convert to object types, returning null if text is null or empty. 731 * 732 * @return converted 733 * @throws StringValueConversionException 734 */ 735 public final Boolean toOptionalBoolean() throws StringValueConversionException 736 { 737 return Strings.isEmpty(text) ? null : toBooleanObject(); 738 } 739 740 /** 741 * Convert to object types, returning null if text is null or empty. 742 * 743 * @return converted 744 * @throws StringValueConversionException 745 */ 746 public final Character toOptionalCharacter() throws StringValueConversionException 747 { 748 return Strings.isEmpty(text) ? null : toCharacter(); 749 } 750 751 /** 752 * Convert to object types, returning null if text is null or empty. 753 * 754 * @return converted 755 * @throws StringValueConversionException 756 */ 757 public final Double toOptionalDouble() throws StringValueConversionException 758 { 759 return Strings.isEmpty(text) ? null : toDoubleObject(); 760 } 761 762 /** 763 * Convert to object types, returning null if text is null or empty. 764 * 765 * @return converted 766 * @throws StringValueConversionException 767 */ 768 public final Duration toOptionalDuration() throws StringValueConversionException 769 { 770 return Strings.isEmpty(text) ? null : toDuration(); 771 } 772 773 /** 774 * Convert to object types, returning null if text is null or empty. 775 * 776 * @return converted 777 * @throws StringValueConversionException 778 */ 779 public final Integer toOptionalInteger() throws StringValueConversionException 780 { 781 return Strings.isEmpty(text) ? null : toInteger(); 782 } 783 784 /** 785 * Convert to object types, returning null if text is null or empty. 786 * 787 * @return converted 788 * @throws StringValueConversionException 789 */ 790 public final Long toOptionalLong() throws StringValueConversionException 791 { 792 return Strings.isEmpty(text) ? null : toLongObject(); 793 } 794 795 /** 796 * Convert to object types, returning null if text is null. 797 * 798 * @return converted 799 */ 800 public final String toOptionalString() 801 { 802 return text; 803 } 804 805 /** 806 * Convert to object types, returning null if text is null or empty. 807 * 808 * @return converted 809 * @throws StringValueConversionException 810 */ 811 public final Instant toOptionalInstant() throws StringValueConversionException 812 { 813 return Strings.isEmpty(text) ? null : toInstant(); 814 } 815 816 /** 817 * @return The string value 818 */ 819 @Override 820 public final String toString() 821 { 822 return text; 823 } 824 825 /** 826 * Convert to primitive types, returning default value if text is null. 827 * 828 * @param defaultValue 829 * the default value to return of text is null 830 * @return the converted text as a primitive or the default if text is null 831 */ 832 public final String toString(final String defaultValue) 833 { 834 return (text == null) ? defaultValue : text; 835 } 836 837 /** 838 * Convert this text to an {@link Instant} instance. 839 * 840 * @return Converted text 841 * @throws StringValueConversionException 842 */ 843 public final Instant toInstant() throws StringValueConversionException 844 { 845 try 846 { 847 return Instant.parse(text); 848 } 849 catch (DateTimeParseException e) 850 { 851 throw new StringValueConversionException( 852 "Unable to convert '" + text + "' to a Instant value", e); 853 } 854 } 855 856 /** 857 * Convert to {@link Instant}, returning default value if text is inconvertible. 858 * 859 * @param defaultValue 860 * the default value 861 * @return the converted text as a {@link Instant} or the default value if text is inconvertible. 862 */ 863 public final Instant toInstant(final Instant defaultValue) 864 { 865 if (text != null) 866 { 867 try 868 { 869 return toInstant(); 870 } 871 catch (StringValueConversionException x) 872 { 873 if (LOG.isDebugEnabled()) 874 { 875 LOG.debug(String.format("An error occurred while converting '%s' to an Instant: %s", 876 text, x.getMessage()), x); 877 } 878 } 879 } 880 return defaultValue; 881 } 882 883 /** 884 * Convert this text to an enum. 885 * 886 * @param eClass 887 * enum type 888 * @return The value as an enum 889 * @throws StringValueConversionException 890 */ 891 public final <T extends Enum<T>> T toEnum(Class<T> eClass) throws StringValueConversionException 892 { 893 return Strings.toEnum(text, eClass); 894 } 895 896 /** 897 * Convert this text to an enum. 898 * 899 * @param defaultValue 900 * This will be returned if there is an error converting the value 901 * @return The value as an enum 902 */ 903 @SuppressWarnings("unchecked") 904 public final <T extends Enum<T>> T toEnum(final T defaultValue) 905 { 906 Args.notNull(defaultValue, "defaultValue"); 907 return toEnum((Class<T>)defaultValue.getClass(), defaultValue); 908 } 909 910 /** 911 * Convert this text to an enum. 912 * 913 * @param eClass 914 * enum type 915 * @param defaultValue 916 * This will be returned if there is an error converting the value 917 * @return The value as an enum 918 */ 919 public final <T extends Enum<T>> T toEnum(Class<T> eClass, final T defaultValue) 920 { 921 if (text != null) 922 { 923 try 924 { 925 return toEnum(eClass); 926 } 927 catch (StringValueConversionException x) 928 { 929 if (LOG.isDebugEnabled()) 930 { 931 LOG.debug(String.format("An error occurred while converting '%s' to a %s: %s", 932 text, eClass, x.getMessage()), x); 933 } 934 } 935 } 936 return defaultValue; 937 } 938 939 /** 940 * Convert to enum, returning null if text is null or empty. 941 * 942 * @param eClass 943 * enum type 944 * 945 * @return converted 946 * @throws StringValueConversionException 947 */ 948 public final <T extends Enum<T>> T toOptionalEnum(Class<T> eClass) 949 throws StringValueConversionException 950 { 951 return Strings.isEmpty(text) ? null : toEnum(eClass); 952 } 953 954 /** 955 * Returns whether the text is null. 956 * 957 * @return <code>true</code> if the text is <code>null</code>, <code>false</code> otherwise. 958 */ 959 public boolean isNull() 960 { 961 return text == null; 962 } 963 964 /** 965 * Returns whether the text is null or empty 966 * 967 * @return <code>true</code> if the text is <code>null</code> or 968 * <code>.trim().length()==0</code>, <code>false</code> otherwise. 969 */ 970 public boolean isEmpty() 971 { 972 return Strings.isEmpty(text); 973 } 974 975 /** 976 * {@inheritDoc} 977 */ 978 @Override 979 public int hashCode() 980 { 981 return Objects.hashCode(locale, text); 982 } 983 984 /** 985 * {@inheritDoc} 986 */ 987 @Override 988 public boolean equals(final Object obj) 989 { 990 if (obj instanceof StringValue) 991 { 992 StringValue stringValue = (StringValue)obj; 993 return Objects.isEqual(text, stringValue.text) && locale.equals(stringValue.locale); 994 } 995 else 996 { 997 return false; 998 } 999 } 1000}