001/*
002 * Copyright 2023 the original author or authors.
003 * <p>
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * https://www.apache.org/licenses/LICENSE-2.0
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package de.cuioss.tools.string;
017
018import static de.cuioss.tools.base.Preconditions.checkArgument;
019
020import java.util.ArrayList;
021import java.util.List;
022import java.util.Optional;
023import java.util.function.Predicate;
024
025import de.cuioss.tools.logging.CuiLogger;
026import lombok.NonNull;
027import lombok.experimental.UtilityClass;
028
029/**
030 * Provides some extensions to String handling like
031 * com.google.common.base.Strings. It was created using different source, see
032 * authors
033 *
034 * @author Oliver Wolff
035 * @author Sven Haag
036 * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java
037 * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/CharSequenceUtils.java
038 * @author https://github.com/spring-projects/spring-framework/blob/v5.1.8.RELEASE/spring-core/src/main/java/org/springframework/util/StringUtils.java
039 * @author com.google.common.base.Strings
040 */
041@UtilityClass
042public final class MoreStrings {
043
044    private static final CuiLogger log = new CuiLogger(MoreStrings.class);
045
046    /**
047     * The empty String {@code ""}.
048     */
049    public static final String EMPTY = "";
050
051    /**
052     * <p>
053     * The maximum size to which the padding constant(s) can expand.
054     * </p>
055     */
056    private static final int PAD_LIMIT = 8192;
057
058    /**
059     * A String for a space character.
060     */
061    public static final String SPACE = " ";
062
063    /**
064     * Represents a failed index search.
065     */
066    public static final int INDEX_NOT_FOUND = -1;
067
068    /**
069     * "Unquotes" a String, saying if the given String starts and ends with the
070     * token "'" or """ the quotes will be stripped
071     *
072     * @param original may be null or empty
073     * @return the unquoted String or the original in none could be found
074     */
075    public static String unquote(final String original) {
076        if (isEmpty(original)) {
077            return original;
078        }
079        if (original.startsWith("\"") && original.endsWith("\"")
080                || original.startsWith("'") && original.endsWith("'")) {
081            return original.substring(1, original.length() - 1);
082        }
083        return original;
084    }
085
086    /**
087     * <p>
088     * Checks if the CharSequence contains only lowercase characters.
089     * </p>
090     *
091     * <p>
092     * {@code null} will return {@code false}. An empty CharSequence (length()=0)
093     * will return {@code false}.
094     * </p>
095     *
096     * <pre>
097     * MoreStrings.isAllLowerCase(null)   = false
098     * MoreStrings.isAllLowerCase("")     = false
099     * MoreStrings.isAllLowerCase("  ")   = false
100     * MoreStrings.isAllLowerCase("abc")  = true
101     * MoreStrings.isAllLowerCase("abC")  = false
102     * MoreStrings.isAllLowerCase("ab c") = false
103     * MoreStrings.isAllLowerCase("ab1c") = false
104     * MoreStrings.isAllLowerCase("ab/c") = false
105     * </pre>
106     *
107     * @param cs the CharSequence to check, may be null
108     *
109     * @return {@code true} if only contains lowercase characters, and is non-null
110     * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java
111     */
112    public static boolean isAllLowerCase(final CharSequence cs) {
113        if (isEmpty(cs)) {
114            return false;
115        }
116        final var sz = cs.length();
117        for (var i = 0; i < sz; i++) {
118            if (!Character.isLowerCase(cs.charAt(i))) {
119                return false;
120            }
121        }
122        return true;
123    }
124
125    /**
126     * <p>
127     * Checks if the CharSequence contains only uppercase characters.
128     * </p>
129     *
130     * <p>
131     * {@code null} will return {@code false}. An empty String (length()=0) will
132     * return {@code false}.
133     * </p>
134     *
135     * <pre>
136     * MoreStrings.isAllUpperCase(null)   = false
137     * MoreStrings.isAllUpperCase("")     = false
138     * MoreStrings.isAllUpperCase("  ")   = false
139     * MoreStrings.isAllUpperCase("ABC")  = true
140     * MoreStrings.isAllUpperCase("aBC")  = false
141     * MoreStrings.isAllUpperCase("A C")  = false
142     * MoreStrings.isAllUpperCase("A1C")  = false
143     * MoreStrings.isAllUpperCase("A/C")  = false
144     * </pre>
145     *
146     * @param cs the CharSequence to check, may be null
147     *
148     * @return {@code true} if only contains uppercase characters, and is non-null
149     * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java
150     */
151    public static boolean isAllUpperCase(final CharSequence cs) {
152        if (isEmpty(cs)) {
153            return false;
154        }
155        final var sz = cs.length();
156        for (var i = 0; i < sz; i++) {
157            if (!Character.isUpperCase(cs.charAt(i))) {
158                return false;
159            }
160        }
161        return true;
162    }
163
164    /**
165     * <p>
166     * Checks if a CharSequence is empty ("") or null.
167     * </p>
168     *
169     * <pre>
170     * MoreStrings.isEmpty(null)      = true
171     * MoreStrings.isEmpty("")        = true
172     * MoreStrings.isEmpty(" ")       = false
173     * MoreStrings.isEmpty("bob")     = false
174     * MoreStrings.isEmpty("  bob  ") = false
175     * </pre>
176     *
177     * @param cs the CharSequence to check, may be null
178     *
179     * @return {@code true} if the CharSequence is empty or null
180     * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java
181     */
182    public static boolean isEmpty(final CharSequence cs) {
183        return cs == null || cs.length() == 0;
184    }
185
186    /**
187     * <p>
188     * Checks if a CharSequence is not empty ("") or null.
189     * </p>
190     *
191     * <pre>
192     * MoreStrings.isEmpty(null)      = false
193     * MoreStrings.isEmpty("")        = false
194     * MoreStrings.isEmpty(" ")       = true
195     * </pre>
196     *
197     * @param cs the CharSequence to check, may be null
198     *
199     * @return {@code true} if the CharSequence is not empty
200     */
201    public static boolean isPresent(final CharSequence cs) {
202        return cs != null && cs.length() > 0;
203    }
204
205    /**
206     * <p>
207     * Checks if the CharSequence contains only Unicode digits. A decimal point is
208     * not a Unicode digit and returns false.
209     * </p>
210     *
211     * <p>
212     * {@code null} will return {@code false}. An empty CharSequence (length()=0)
213     * will return {@code false}.
214     * </p>
215     *
216     * <p>
217     * Note that the method does not allow for a leading sign, either positive or
218     * negative. Also, if a String passes the numeric test, it may still generate a
219     * NumberFormatException when parsed by Integer.parseInt or Long.parseLong, e.g.
220     * if the value is outside the range for int or long respectively.
221     * </p>
222     *
223     * <pre>
224     * MoreStrings.isNumeric(null)   = false
225     * MoreStrings.isNumeric("")     = false
226     * MoreStrings.isNumeric("  ")   = false
227     * MoreStrings.isNumeric("123")  = true
228     * MoreStrings.isNumeric("१२३")  = true MoreStrings.isNumeric("12 3") = false MoreStrings.isNumeric("ab2c") = false MoreStrings.isNumeric("12-3") = false MoreStrings.isNumeric("12.3") = false MoreStrings.isNumeric("-123") = false MoreStrings.isNumeric("+123") = false
229     * </pre >
230     *
231     * @param csthe CharSequence to check, may be null@return{@codetrue if only
232     *              contains digits, and is non-null@author
233     *              https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java
234     *
235     *
236     *
237     */
238    public static boolean isNumeric(final CharSequence cs) {
239        if (isEmpty(cs)) {
240            return false;
241        }
242        final var sz = cs.length();
243        for (var i = 0; i < sz; i++) {
244            if (!Character.isDigit(cs.charAt(i))) {
245                return false;
246            }
247        }
248        return true;
249    }
250
251    /**
252     * <p>
253     * Checks if a CharSequence is empty (""), null or whitespace only.
254     * </p>
255     *
256     * <p>
257     * Whitespace is defined by {@link Character#isWhitespace(char)}.
258     * </p>
259     *
260     * <pre>
261     * MoreStrings.isBlank(null)      = true
262     * MoreStrings.isBlank("")        = true
263     * MoreStrings.isBlank(" ")       = true
264     * MoreStrings.isBlank("bob")     = false
265     * MoreStrings.isBlank("  bob  ") = false
266     * </pre>
267     *
268     * @param cs the CharSequence to check, may be null
269     *
270     * @return {@code true} if the CharSequence is null, empty or whitespace only
271     * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java
272     */
273    public static boolean isBlank(final CharSequence cs) {
274        final int strLen;
275        if (cs == null || (strLen = cs.length()) == 0) {
276            return true;
277        }
278        for (var i = 0; i < strLen; i++) {
279            if (!Character.isWhitespace(cs.charAt(i))) {
280                return false;
281            }
282        }
283        return true;
284    }
285
286    /**
287     * {@code NOT} of {@link #isBlank(CharSequence)}.
288     *
289     * <p>
290     * Checks if a CharSequence is not empty (""), null or contains whitespaces
291     * only.
292     * </p>
293     *
294     * <p>
295     * Whitespace is defined by {@link Character#isWhitespace(char)}.
296     * </p>
297     *
298     * <pre>
299     * MoreStrings.isBlank(null)      = false
300     * MoreStrings.isBlank("")        = false
301     * MoreStrings.isBlank(" ")       = false
302     * MoreStrings.isBlank("bob")     = true
303     * MoreStrings.isBlank("  bob  ") = true
304     * </pre>
305     *
306     * @param cs to be checked
307     * @return {@code true} if the given string is no blank, {@code false} otherwise
308     */
309    public static boolean isNotBlank(final CharSequence cs) {
310        return !isBlank(cs);
311    }
312
313    /**
314     * <p>
315     * Counts how many times the substring appears in the larger string.
316     * </p>
317     *
318     * <p>
319     * A {@code null} or empty ("") String input returns {@code 0}.
320     * </p>
321     *
322     * <pre>
323     * MoreStrings.countMatches(null, *)       = 0
324     * MoreStrings.countMatches("", *)         = 0
325     * MoreStrings.countMatches("abba", null)  = 0
326     * MoreStrings.countMatches("abba", "")    = 0
327     * MoreStrings.countMatches("abba", "a")   = 2
328     * MoreStrings.countMatches("abba", "ab")  = 1
329     * MoreStrings.countMatches("abba", "xxx") = 0
330     * </pre>
331     *
332     *
333     * @param str the CharSequence to check, may be null
334     * @param sub the substring to count, may be null
335     * @return the number of occurrences, 0 if either CharSequence is {@code null}
336     * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java
337     */
338    public static int countMatches(final CharSequence str, final CharSequence sub) {
339        if (isEmpty(str) || isEmpty(sub)) {
340            return 0;
341        }
342        var count = 0;
343        var idx = 0;
344        while ((idx = indexOf(str, sub, idx)) != INDEX_NOT_FOUND) {
345            count++;
346            idx += sub.length();
347        }
348        return count;
349    }
350
351    /**
352     * <p>
353     * Left pad a String with spaces (' ').
354     * </p>
355     *
356     * <p>
357     * The String is padded to the size of {@code size}.
358     * </p>
359     *
360     * <pre>
361     * MoreStrings.leftPad(null, *)   = null
362     * MoreStrings.leftPad("", 3)     = "   "
363     * MoreStrings.leftPad("bat", 3)  = "bat"
364     * MoreStrings.leftPad("bat", 5)  = "  bat"
365     * MoreStrings.leftPad("bat", 1)  = "bat"
366     * MoreStrings.leftPad("bat", -1) = "bat"
367     * </pre>
368     *
369     * @param str  the String to pad out, may be null
370     * @param size the size to pad to
371     * @return left padded String or original String if no padding is necessary,
372     *         {@code null} if null String input
373     * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java
374     */
375    public static String leftPad(final String str, final int size) {
376        return leftPad(str, size, ' ');
377    }
378
379    /**
380     * <p>
381     * Left pad a String with a specified character.
382     * </p>
383     *
384     * <p>
385     * Pad to a size of {@code size}.
386     * </p>
387     *
388     * <pre>
389     * MoreStrings.leftPad(null, *, *)     = null
390     * MoreStrings.leftPad("", 3, 'z')     = "zzz"
391     * MoreStrings.leftPad("bat", 3, 'z')  = "bat"
392     * MoreStrings.leftPad("bat", 5, 'z')  = "zzbat"
393     * MoreStrings.leftPad("bat", 1, 'z')  = "bat"
394     * MoreStrings.leftPad("bat", -1, 'z') = "bat"
395     * </pre>
396     *
397     * @param str     the String to pad out, may be null
398     * @param size    the size to pad to
399     * @param padChar the character to pad with
400     * @return left padded String or original String if no padding is necessary,
401     *         {@code null} if null String input
402     * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java
403     */
404    public static String leftPad(final String str, final int size, final char padChar) {
405        if (str == null) {
406            return null;
407        }
408        final var pads = size - str.length();
409        if (pads <= 0) {
410            return str; // returns original String when possible
411        }
412        if (pads > PAD_LIMIT) {
413            return leftPad(str, size, String.valueOf(padChar));
414        }
415        return repeat(padChar, pads).concat(str);
416    }
417
418    /**
419     * <p>
420     * Left pad a String with a specified String.
421     * </p>
422     *
423     * <p>
424     * Pad to a size of {@code size}.
425     * </p>
426     *
427     * <pre>
428     * MoreStrings.leftPad(null, *, *)      = null
429     * MoreStrings.leftPad("", 3, "z")      = "zzz"
430     * MoreStrings.leftPad("bat", 3, "yz")  = "bat"
431     * MoreStrings.leftPad("bat", 5, "yz")  = "yzbat"
432     * MoreStrings.leftPad("bat", 8, "yz")  = "yzyzybat"
433     * MoreStrings.leftPad("bat", 1, "yz")  = "bat"
434     * MoreStrings.leftPad("bat", -1, "yz") = "bat"
435     * MoreStrings.leftPad("bat", 5, null)  = "  bat"
436     * MoreStrings.leftPad("bat", 5, "")    = "  bat"
437     * </pre>
438     *
439     * @param str    the String to pad out, may be null
440     * @param size   the size to pad to
441     * @param padStr the String to pad with, null or empty treated as single space
442     * @return left padded String or original String if no padding is necessary,
443     *         {@code null} if null String input
444     * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java
445     */
446    public static String leftPad(final String str, final int size, String padStr) {
447        if (str == null) {
448            return null;
449        }
450        if (isEmpty(padStr)) {
451            padStr = SPACE;
452        }
453        final var padLen = padStr.length();
454        final var strLen = str.length();
455        final var pads = size - strLen;
456        if (pads <= 0) {
457            return str; // returns original String when possible
458        }
459        if (padLen == 1 && pads <= PAD_LIMIT) {
460            return leftPad(str, size, padStr.charAt(0));
461        }
462
463        if (pads == padLen) {
464            return padStr.concat(str);
465        }
466        if (pads < padLen) {
467            return padStr.substring(0, pads).concat(str);
468        }
469        final var padding = new char[pads];
470        final var padChars = padStr.toCharArray();
471        for (var i = 0; i < pads; i++) {
472            padding[i] = padChars[i % padLen];
473        }
474        return new String(padding).concat(str);
475    }
476
477    /**
478     * <p>
479     * Returns padding using the specified delimiter repeated to a given length.
480     * </p>
481     *
482     * <pre>
483     * MoreStrings.repeat('e', 0)  = ""
484     * MoreStrings.repeat('e', 3)  = "eee"
485     * MoreStrings.repeat('e', -2) = ""
486     * </pre>
487     *
488     * <p>
489     * Note: this method does not support padding with
490     * <a href="http://www.unicode.org/glossary/#supplementary_character">Unicode
491     * Supplementary Characters</a> as they require a pair of {@code char}s to be
492     * represented. If you are needing to support full I18N of your applications
493     * consider using <code>repeat(String, int)</code> instead.
494     * </p>
495     *
496     * @param ch     character to repeat
497     * @param repeat number of times to repeat char, negative treated as zero
498     * @return String with repeated character
499     * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java
500     */
501    public static String repeat(final char ch, final int repeat) {
502        if (repeat <= 0) {
503            return EMPTY;
504        }
505        final var buf = new char[repeat];
506        for (var i = repeat - 1; i >= 0; i--) {
507            buf[i] = ch;
508        }
509        return new String(buf);
510    }
511
512    /**
513     * <p>
514     * Strips any of a set of characters from the end of a String.
515     * </p>
516     *
517     * <p>
518     * A {@code null} input String returns {@code null}. An empty string ("") input
519     * returns the empty string.
520     * </p>
521     *
522     * <p>
523     * If the stripChars String is {@code null}, whitespace is stripped as defined
524     * by {@link Character#isWhitespace(char)}.
525     * </p>
526     *
527     * <pre>
528     * MoreStrings.stripEnd(null, *)          = null
529     * MoreStrings.stripEnd("", *)            = ""
530     * MoreStrings.stripEnd("abc", "")        = "abc"
531     * MoreStrings.stripEnd("abc", null)      = "abc"
532     * MoreStrings.stripEnd("  abc", null)    = "  abc"
533     * MoreStrings.stripEnd("abc  ", null)    = "abc"
534     * MoreStrings.stripEnd(" abc ", null)    = " abc"
535     * MoreStrings.stripEnd("  abcyx", "xyz") = "  abc"
536     * MoreStrings.stripEnd("120.00", ".0")   = "12"
537     * </pre>
538     *
539     *
540     * @param str        the String to remove characters from, may be null
541     * @param stripChars the set of characters to remove, null treated as whitespace
542     * @return the stripped String, {@code null} if null String input
543     * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java
544     */
545    public static String stripEnd(final String str, final String stripChars) {
546        int end;
547        if (str == null || (end = str.length()) == 0) {
548            return str;
549        }
550
551        if (stripChars == null) {
552            while (end != 0 && Character.isWhitespace(str.charAt(end - 1))) {
553                end--;
554            }
555        } else if (stripChars.isEmpty()) {
556            return str;
557        } else {
558            while (end != 0 && stripChars.indexOf(str.charAt(end - 1)) != INDEX_NOT_FOUND) {
559                end--;
560            }
561        }
562        return str.substring(0, end);
563    }
564
565    /**
566     * Returns the index within <code>seq</code> of the first occurrence of the
567     * specified character. If a character with value <code>searchChar</code> occurs
568     * in the character sequence represented by <code>seq</code>
569     * <code>CharSequence</code> object, then the index (in Unicode code units) of
570     * the first such occurrence is returned. For values of <code>searchChar</code>
571     * in the range from 0 to 0xFFFF (inclusive), this is the smallest value
572     * <i>k</i> such that: <blockquote>
573     *
574     * <pre>
575     * this.charAt(<i>k</i>) == searchChar
576     * </pre>
577     *
578     * </blockquote> is true. For other values of <code>searchChar</code>, it is the
579     * smallest value <i>k</i> such that: <blockquote>
580     *
581     * <pre>
582     * this.codePointAt(<i>k</i>) == searchChar
583     * </pre>
584     *
585     * </blockquote> is true. In either case, if no such character occurs in
586     * <code>seq</code>, then {@code INDEX_NOT_FOUND (-1)} is returned.
587     *
588     * <p>
589     * Furthermore, a {@code null} or empty ("") CharSequence will return
590     * {@code INDEX_NOT_FOUND (-1)}.
591     * </p>
592     *
593     * <pre>
594     * MoreStrings.indexOf(null, *)         = -1
595     * MoreStrings.indexOf("", *)           = -1
596     * MoreStrings.indexOf("aabaabaa", 'a') = 0
597     * MoreStrings.indexOf("aabaabaa", 'b') = 2
598     * </pre>
599     *
600     *
601     * @param seq        the CharSequence to check, may be null
602     * @param searchChar the character to find
603     * @return the first index of the search character, -1 if no match or
604     *         {@code null} string input
605     * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java
606     */
607    public static int indexOf(final CharSequence seq, final int searchChar) {
608        if (isEmpty(seq)) {
609            return INDEX_NOT_FOUND;
610        }
611        return indexOf(seq, searchChar, 0);
612    }
613
614    /**
615     * Returns the index within <code>cs</code> of the first occurrence of the
616     * specified character, starting the search at the specified index.
617     * <p>
618     * If a character with value <code>searchChar</code> occurs in the character
619     * sequence represented by the <code>cs</code> object at an index no smaller
620     * than <code>start</code>, then the index of the first such occurrence is
621     * returned. For values of <code>searchChar</code> in the range from 0 to 0xFFFF
622     * (inclusive), this is the smallest value <i>k</i> such that: <blockquote>
623     *
624     * <pre>
625     * (this.charAt(<i>k</i>) == searchChar) &amp;&amp; (<i>k</i> &gt;= start)
626     * </pre>
627     *
628     * </blockquote> is true. For other values of <code>searchChar</code>, it is the
629     * smallest value <i>k</i> such that: <blockquote>
630     *
631     * <pre>
632     * (this.codePointAt(<i>k</i>) == searchChar) &amp;&amp; (<i>k</i> &gt;= start)
633     * </pre>
634     *
635     * </blockquote> is true. In either case, if no such character occurs inm
636     * <code>cs</code> at or after position <code>start</code>, then <code>-1</code>
637     * is returned.
638     *
639     * <p>
640     * There is no restriction on the value of <code>start</code>. If it is
641     * negative, it has the same effect as if it were zero: the entire
642     * <code>CharSequence</code> may be searched. If it is greater than the length
643     * of <code>cs</code>, it has the same effect as if it were equal to the length
644     * of <code>cs</code>: <code>-1</code> is returned.
645     *
646     * <p>
647     * All indices are specified in <code>char</code> values (Unicode code units).
648     *
649     * @param cs         the {@code CharSequence} to be processed, not null
650     * @param searchChar the char to be searched for
651     * @param start      the start index, negative starts at the string start
652     * @return the index where the search char was found, -1 if not found
653     * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java
654     */
655    @SuppressWarnings("squid:S3776") // owolff: original code
656    public static int indexOf(final CharSequence cs, final int searchChar, int start) {
657        if (isEmpty(cs)) {
658            return INDEX_NOT_FOUND;
659        }
660
661        if (cs instanceof String string) {
662            return string.indexOf(searchChar, start);
663        }
664        final var sz = cs.length();
665        if (start < 0) {
666            start = 0;
667        }
668        if (searchChar < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
669            for (var i = start; i < sz; i++) {
670                if (cs.charAt(i) == searchChar) {
671                    return i;
672                }
673            }
674        }
675        // supplementary characters (LANG1300)
676        if (searchChar <= Character.MAX_CODE_POINT) {
677            final var chars = Character.toChars(searchChar);
678            for (var i = start; i < sz - 1; i++) {
679                final var high = cs.charAt(i);
680                final var low = cs.charAt(i + 1);
681                if (high == chars[0] && low == chars[1]) {
682                    return i;
683                }
684            }
685        }
686        return INDEX_NOT_FOUND;
687    }
688
689    /**
690     * Used by the indexOf(CharSequence methods) as a green implementation of
691     * indexOf.
692     *
693     * @param cs         the {@code CharSequence} to be processed
694     * @param searchChar the {@code CharSequence} to be searched for
695     * @param start      the start index
696     * @return the index where the search sequence was found
697     *
698     * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java
699     */
700    public static int indexOf(final CharSequence cs, final CharSequence searchChar, final int start) {
701        if (cs == null || searchChar == null) {
702            return INDEX_NOT_FOUND;
703        }
704        return cs.toString().indexOf(searchChar.toString(), start);
705    }
706
707    /**
708     * Check whether the given {@code String} contains actual <em>text</em>.
709     * <p>
710     * More specifically, this method returns {@code true} if the {@code String} is
711     * not {@code null}, its length is greater than 0, and it contains at least one
712     * non-whitespace character.
713     *
714     * @param str the {@code String} to check (maybe {@code null})
715     *
716     * @return {@code true} if the {@code String} is not {@code null}, its length is
717     *         greater than 0, and it does not contain whitespace only
718     * @author https://github.com/spring-projects/spring-framework/blob/v5.1.8.RELEASE/spring-core/src/main/java/org/springframework/util/StringUtils.java
719     */
720    public static boolean hasNonWhitespaceChar(final CharSequence str) {
721        if (isEmpty(str)) {
722            return false;
723        }
724
725        final var strLen = str.length();
726        for (var i = 0; i < strLen; i++) {
727            if (!Character.isWhitespace(str.charAt(i))) {
728                return true;
729            }
730        }
731        return false;
732    }
733
734    /**
735     * Checks a string for being non-null and not empty (checks without trimming)
736     * Throws an {@link IllegalArgumentException} if String is null or empty
737     *
738     * @param underCheck
739     * @return the given String
740     */
741    public static String requireNotEmpty(final String underCheck) {
742        checkArgument(!isEmpty(underCheck), "Given String is Empty");
743        return underCheck;
744    }
745
746    /**
747     * Checks a string for being non-null and not empty (checks without trimming)
748     * Throws an {@link IllegalArgumentException} if String is null or empty
749     *
750     * @param underCheck
751     * @param attributeName used for the creation of the error text
752     * @return the given String
753     */
754    public static String requireNotEmpty(final String underCheck, final String attributeName) {
755        checkArgument(!isEmpty(underCheck), "Attribute with name '" + attributeName + "' must not be empty");
756        return underCheck;
757    }
758
759    /**
760     * Checks a string for being non-null and not empty (checks with trimming)
761     * Throws an {@link IllegalArgumentException} if String is null or empty
762     *
763     * @param underCheck
764     * @return the given String
765     */
766    public static String requireNotEmptyTrimmed(final String underCheck) {
767        checkArgument(!isBlank(underCheck), "Attribute must not be blank");
768        return underCheck;
769    }
770
771    /**
772     * Checks a string for being non-null and not empty (checks with trimming)
773     * Throws an {@link IllegalArgumentException} if String is null or empty
774     *
775     * @param underCheck
776     * @param attributeName used for the creation of the error text
777     * @return the given String
778     */
779    public static String requireNotEmptyTrimmed(final String underCheck, final String attributeName) {
780        checkArgument(!isBlank(underCheck), "Attribute with name '" + attributeName + "' must not be blank");
781        return underCheck;
782    }
783
784    /**
785     * Returns the given string if it is non-null; the empty string otherwise.
786     *
787     * @author com.google.common.base.Strings
788     * @param string the string to test and possibly return
789     * @return {@code string} itself if it is non-null; {@code ""} if it is null
790     */
791    public static String nullToEmpty(String string) {
792        if (null == string) {
793            return "";
794        }
795        return string;
796    }
797
798    /**
799     * Returns the given string if it is nonempty; {@code null} otherwise.
800     *
801     * @author com.google.common.base.Strings
802     * @param string the string to test and possibly return
803     * @return {@code string} itself if it is nonempty; {@code null} if it is empty
804     *         or null
805     */
806    public static String emptyToNull(String string) {
807        if (null == string || string.isEmpty()) {
808            return null;
809        }
810        return string;
811    }
812
813    /**
814     * Null-safe trimming of a String value.
815     *
816     * @param string to be trimmed
817     *
818     * @return <code>null</code> if the string is <code>null</code> otherwise the
819     *         trimmed string
820     */
821    public static String trimOrNull(final String string) {
822        return null != string ? string.trim() : null;
823    }
824
825    /**
826     * Returns the given {@code template} string with each occurrence of
827     * {@code "%s"} replaced with the corresponding argument value from
828     * {@code args}; or, if the placeholder and argument counts do not match,
829     * returns a best-effort form of that string. Will not throw an exception under
830     * normal conditions.
831     *
832     * <p>
833     * <b>Note:</b> For most string-formatting needs, use {@link String#format
834     * String.format}, {@link java.io.PrintWriter#format PrintWriter.format}, and
835     * related methods. These support the full range of <a href=
836     * "https://docs.oracle.com/javase/9/docs/api/java/util/Formatter.html#syntax">format
837     * specifiers</a>, and alert you to usage errors by throwing
838     * {@link java.util.IllegalFormatException}.
839     *
840     * <p>
841     * In certain cases, such as outputting debugging information or constructing a
842     * message to be used for another unchecked exception, an exception during
843     * string formatting would serve little purpose except to supplant the real
844     * information you were trying to provide. These are the cases this method is
845     * made for; it instead generates a best-effort string with all supplied
846     * argument values present. This method is also useful in environments such as
847     * GWT where {@code String.format} is not available.
848     *
849     * <p>
850     * <b>Warning:</b> Only the exact two-character placeholder sequence
851     * {@code "%s"} is recognized.
852     *
853     * @author com.google.common.base.Strings
854     * @param template a string containing zero or more {@code "%s"} placeholder
855     *                 sequences. {@code
856     *     null}    is treated as the four-character string {@code "null"}.
857     * @param args     the arguments to be substituted into the message template.
858     *                 The first argument specified is substituted for the first
859     *                 occurrence of {@code "%s"} in the template, and so forth. A
860     *                 {@code null} argument is converted to the four-character
861     *                 string {@code "null"}; non-null values are converted to
862     *                 strings using {@link Object#toString()}.
863     * @return the resulting formatting String
864     */
865    public static String lenientFormat(String template, Object... args) {
866        template = String.valueOf(template); // null -> "null"
867
868        final List<Object> lenientArgs;
869
870        if (args == null) {
871            lenientArgs= new ArrayList<>(1);
872            lenientArgs.add("(Object[])null");
873        } else {
874            lenientArgs = new ArrayList<>(args.length);
875            for (Object arg : args) {
876                lenientArgs.add(lenientToString(arg));
877            }
878        }
879
880        // start substituting the arguments into the '%s' placeholders
881        StringBuilder builder = new StringBuilder(template.length() + 16 * lenientArgs.size());
882        int templateStart = 0;
883        int i = 0;
884        while (i < lenientArgs.size()) {
885            int placeholderStart = template.indexOf("%s", templateStart);
886            if (placeholderStart == -1) {
887                break;
888            }
889            builder.append(template, templateStart, placeholderStart);
890            builder.append(lenientArgs.get(i++));
891            templateStart = placeholderStart + 2;
892        }
893        builder.append(template, templateStart, template.length());
894
895        // if we run out of placeholders, append the extra args in square braces
896        if (i < lenientArgs.size()) {
897            builder.append(" [");
898            builder.append(lenientArgs.get(i++));
899            while (i < lenientArgs.size()) {
900                builder.append(", ");
901                builder.append(lenientArgs.get(i++));
902            }
903            builder.append(']');
904        }
905
906        return builder.toString();
907    }
908
909    /**
910     * @param value  to be processed
911     * @param suffix to be present
912     * @return value with suffix
913     */
914    public static String ensureEndsWith(@NonNull final String value, @NonNull final String suffix) {
915        if (!value.endsWith(suffix)) {
916            return value + suffix;
917        }
918        return value;
919    }
920
921    /**
922     * @author com.google.common.base.Strings
923     */
924    static String lenientToString(Object o) {
925        try {
926            return String.valueOf(o);
927        } catch (Exception e) {
928            // Default toString() behavior - see Object.toString()
929            var objectToString = o.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(o));
930            log.warn(e, "Exception during lenientFormat for {}", objectToString);
931            return "<" + objectToString + " threw " + e.getClass().getName() + ">";
932        }
933    }
934
935    /**
936     * @param checker the predicate to check each given value against. it decides if
937     *                a value qualifies to be returned.
938     * @param values  to be evaluated
939     * @return first string that is accepted by the given {@link Predicate} or
940     *         {@link Optional#empty()}
941     */
942    public static Optional<String> coalesce(Predicate<String> checker, String... values) {
943        if (null != values) {
944            for (String value : values) {
945                if (!checker.test(value)) {
946                    return Optional.of(value);
947                }
948            }
949        }
950        return Optional.empty();
951    }
952
953    /**
954     * @param values to be evaluated
955     * @return first string that is not {@link #isEmpty(CharSequence)}
956     */
957    public static Optional<String> firstNonEmpty(String... values) {
958        return coalesce(MoreStrings::isEmpty, values);
959    }
960
961    /**
962     * @param values to be evaluated
963     * @return first string that is not {@link #isBlank(CharSequence)}
964     */
965    public static Optional<String> firstNonBlank(String... values) {
966        return coalesce(MoreStrings::isBlank, values);
967    }
968}