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}