content: /*
   *  Licensed to the Apache Software Foundation (ASF) under one
   *  or more contributor license agreements.  See the NOTICE file
   *  distributed with this work for additional information
   *  regarding copyright ownership.  The ASF licenses this file
   *  to you under the Apache License, Version 2.0 (the
   *  "License"); you may not use this file except in compliance
   *  with the License.  You may obtain a copy of the License at
   *
   *        http://www.apache.org/licenses/LICENSE-2.0
   *
   *  Unless required by applicable law or agreed to in writing,
   *  software distributed under the License is distributed on an
   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   *  KIND, either express or implied.  See the License for the
   *  specific language governing permissions and limitations
   *  under the License.
   */
  package org.apache.causeway.commons.internal.base;
  
  import java.io.ByteArrayOutputStream;
  import java.io.InputStream;
  import java.io.PrintStream;
  import java.net.URI;
  import java.nio.charset.Charset;
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Comparator;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Optional;
  import java.util.Scanner;
  import java.util.Spliterator;
  import java.util.Spliterators;
  import java.util.function.BiConsumer;
  import java.util.function.BiFunction;
  import java.util.function.Consumer;
  import java.util.function.Predicate;
  import java.util.function.UnaryOperator;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  import java.util.stream.IntStream;
  import java.util.stream.Stream;
  import java.util.stream.StreamSupport;
  
  import org.jspecify.annotations.Nullable;
  
  import org.apache.causeway.commons.collections.Can;
  import org.apache.causeway.commons.internal._Constants;
  import org.apache.causeway.commons.internal.assertions._Assert;
  import org.apache.causeway.commons.internal.base._Bytes.BytesOperator;
  import org.apache.causeway.commons.internal.functions._Predicates;
  
  import static org.apache.causeway.commons.internal.base._NullSafe.size;
  import static org.apache.causeway.commons.internal.base._Strings_SplitIterator.splitIterator;
  import static org.apache.causeway.commons.internal.functions._Predicates.not;
  
  import org.jspecify.annotations.NonNull;
  import lombok.SneakyThrows;
  import lombok.experimental.UtilityClass;
  
  /**
   * <h1>- internal use only -</h1>
   * <p>
   * Provides String related algorithms.
   * </p><p>
   * Keep the public methods simple, these are basic building blocks for more complex composites.
   * Composites are provided as static fields.
   * </p>
   * <p>
   * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/>
   * These may be changed or removed without notice!
   * </p>
   *
   * @since 2.0
   */
  @UtilityClass
  public final class _Strings {
  
      // -- CONSTANTS
  
      /**
       * Convenient e.g. for toArray conversions
       * (a duplicate of in {@link _Constants#emptyStringArray} )
       */
      public static final String[] emptyArray = new String[0];
  
      // -- PAIR OF STRINGS
  
      public static interface KeyValuePair {
          String key();
          String value();
      }
  
      public static KeyValuePair pair(final String key, final String value){
          return new _Strings_KeyValuePair(key, value);
      }
  
      /**
       * Parses a string assumed to be of the form <kbd>key[separator]value</kbd> into its parts.
       * @param keyValueLiteral
       * @param separator
       * @return a non-empty Optional, if (and only if) the {@code keyValueLiteral}
       * does contain at least one {@code separator}
       */
      public static Optional<KeyValuePair> parseKeyValuePair(final @Nullable String keyValueLiteral, final char separator) {
          return _Strings_KeyValuePair.parse(keyValueLiteral, separator);
      }
  
      // -- FILLING
  
      public static String of(final int length, final char c) {
          if(length<=0) {
              return "";
          }
          final char[] chars = new char[length];
          Arrays.fill(chars, c);
          return String.valueOf(chars);
      }
  
      // -- COMPARE
  
      /**
       * Compares two strings lexicographically (and nulls-frist).
       * @apiNote consider using {@link Comparator#naturalOrder()} combined with
       * {@link Comparator#nullsFirst(Comparator)}.
       * @implNote this utility method does not produce objects on the heap
       * @param a
       * @param b
       * @return {@code -1} if {@code a < b}, {@code 1} if {@code a > b} else {@code 0}
       * @see String#compareTo(String)
       */
      public static int compareNullsFirst(final @Nullable String a, final @Nullable String b) {
          if(Objects.equals(a, b)) {
              return 0;
          }
          // at this point not both can be null, so which ever is null wins
          if(a==null) {
              return -1;
          }
          if(b==null) {
              return 1;
          }
          // at this point neither can be null
          return a.compareTo(b);
      }
  
      /**
       * Compares two strings lexicographically (and nulls-last).
       * @apiNote consider using {@link Comparator#naturalOrder()} combined with
       * {@link Comparator#nullsFirst(Comparator)}.
       * @implNote this utility method does not produce objects on the heap
       * @param a
       * @param b
       * @return {@code -1} if {@code a < b}, {@code 1} if {@code a > b} else {@code 0}
       * @see String#compareTo(String)
       */
      public static int compareNullsLast(final @Nullable String a, final @Nullable String b) {
          if(Objects.equals(a, b)) {
              return 0;
          }
          // at this point not both can be null, so which ever is null wins
          if(a==null) {
              return 1;
          }
          if(b==null) {
              return -1;
          }
          // at this point neither can be null
          return a.compareTo(b);
      }
  
      // -- BASIC PREDICATES
  
      /**
       * Same as {@link #isNullOrEmpty(CharSequence)}
       * @param x
       * @return true only if string is of zero length or null.
       */
      public static boolean isEmpty(final @Nullable CharSequence x){
          return x==null || x.length()==0;
      }
      /**
       * Same as {@link #isEmpty(CharSequence)}
       * @param x
       * @return true only if string is of zero length or null.
       */
      public static boolean isNullOrEmpty(final @Nullable CharSequence x){
          return x==null || x.length()==0;
      }
  
      /**
       *
       * @param x
       * @return inverse of isEmpty(CharSequence).
       */
      public static boolean isNotEmpty(final @Nullable CharSequence x){
          return x!=null && x.length()!=0;
      }
  
      // -- OPTIONAL
  
      /**
       * @param x - input string
       * @return optionally the input string based on whether the input is not empty
       */
      public static Optional<String> nonEmpty(final @Nullable CharSequence x) {
          return isEmpty(x) ? Optional.empty() : Optional.of(x.toString());
      }
  
      // -- BASIC UNARY OPERATORS
  
      /**
       * @param input
       * @return null if the {@code input} is null or empty, the {@code input} otherwise
       */
      public static @Nullable String emptyToNull(final @Nullable String input) {
          if(isEmpty(input)) {
              return null;
          }
          return input;
      }
  
      /**
       * @param input
       * @return null if the {@code input} is null or blank, the trimmed {@code input} otherwise
       */
      public static @Nullable String blankToNullOrTrim(final @Nullable String input) {
          if(isEmpty(input)) {
              return null;
          }
          return input.isBlank()
                  ? null
                  : input.trim();
      }
  
      /**
       * @param input
       * @return the empty string if the {@code input} is null, the {@code input} otherwise
       */
      public static String nullToEmpty(final @Nullable String input) {
          if(input==null) {
              return "";
          }
          return input;
      }
  
      /**
       * Trims the input.
       * @param input
       * @return null if the {@code input} is null
       */
      public static String trim(final @Nullable String input) {
          return mapIfPresentElseNull(input, String::trim);
      }
  
      /**
       * Converts all of the characters in {@code input} to lower case using the rules of the default locale.
       * @param input
       * @return null if {@code input} is null
       */
      public static String lower(final @Nullable String input) {
          return mapIfPresentElseNull(input, String::toLowerCase);
      }
  
      /**
       * Converts all of the characters in {@code input} to upper case using the rules of the default locale.
       * @param input
       * @return null if {@code input} is null
       */
      public static String upper(final @Nullable String input) {
          return mapIfPresentElseNull(input, String::toUpperCase);
      }
  
      /**
       * Converts the first character in {@code input} to upper case using the rules of the default locale.
       * @param input
       * @return null if {@code input} is null
       */
      public static @Nullable String capitalize(final @Nullable String input) {
          if(input==null) {
              return null;
          }
          if (input.length() == 0) {
              return input;
          }
          if (input.length() == 1) {
              return input.toUpperCase();
          }
          return Character.toUpperCase(input.charAt(0)) + input.substring(1);
      }
  
      /**
       * Converts the first character in {@code input} to lower case using the rules of the default locale.
       * @param input
       * @return null if {@code input} is null
       */
      public static @Nullable String decapitalize(final @Nullable String input) {
          if(input==null) {
              return null;
          }
          if (input.length() == 0) {
              return input;
          }
          if (input.length() == 1) {
              return input.toLowerCase();
          }
          return Character.toLowerCase(input.charAt(0)) + input.substring(1);
      }
  
      // -- SPECIAL UNARY OPERATORS
  
      /**
       * String not-empty (nor null) guard.
       * @param str target for the non-empty-check
       * @param identifier to use for the exception message, when the non-empty-check fails
       * @throws NullPointerException if {@code str} is {@code null}
       * @throws IllegalArgumentException if {@code str} is 'empty'
       */
      public static String requireNonEmpty(final @Nullable String str, final String identifier) {
          if (str == null) {
              throw new NullPointerException(String.format("'%s' is required to be present (not null).", identifier));
          }
          if (str.length()==0) {
              throw new IllegalArgumentException(String.format("'%s' is required to be present and not empty.", identifier));
          }
          return str;
      }
  
      public static @Nullable String htmlEscape(final @Nullable String source) {
          return _Strings_HtmlEscaper.htmlEscape(source);
      }
  
      // -- URL-SAFETY
  
      /**
       * @see "https://stackoverflow.com/a/4571518/9269480"
       */
      public static boolean isUrlSafe(final @Nullable String input) {
          if(_Strings.isEmpty(input)) {
              return true;
          }
          try {
              var testDummyUri = new URI("http://localhost/?" + input);
              var asQuery = testDummyUri.getQuery();
              return input.equals(asQuery);
          } catch (Exception e) {
              // ignore
          }
          return false;
      }
  
      // -- INTERPOLATION
  
      /**
       * String interpolation based on named parameters of pattern <pre>${key} -> value</pre>
       * @see "https://www.baeldung.com/java-string-formatting-named-placeholders#2-creating-the-method"
       */
      public static String format(final String template, final Map<String, Object> parameters) {
          final StringBuilder newTemplate = new StringBuilder(template);
          final List<Object> valueList = new ArrayList<>();
          final Matcher matcher = Pattern.compile("[$][{](\\w+)}").matcher(template);
  
          while (matcher.find()) {
              String key = matcher.group(1);
  
              String paramName = "${" + key + "}";
              int index = newTemplate.indexOf(paramName);
              if (index != -1) {
                  newTemplate.replace(index, index + paramName.length(), "%s");
                  valueList.add(parameters.get(key));
              }
          }
  
          return String.format(newTemplate.toString(), valueList.toArray());
      }
  
      // -- PREFIX/SUFFIX
  
      /**
       * If {@code input} does not start with {@code prefix} prepends {@code prefix} to the input.
       * @param input
       * @param prefix
       * @return null if {@code input} is null
       */
      public static @Nullable String prefix(final @Nullable String input, final @NonNull String prefix) {
          if(input==null) {
              return null;
          }
          if(input.startsWith(prefix)) {
              return input;
          }
          return prefix + input;
      }
  
      /**
       * If {@code input} does not end with {@code suffix} appends {@code suffix} to the input.
       * @param input
       * @param suffix
       * @return null if {@code input} is null
       */
      public static @Nullable String suffix(final @Nullable String input, final @NonNull String suffix) {
          if(input==null) {
              return null;
          }
          if(input.endsWith(suffix)) {
              return input;
          }
          return input + suffix;
      }
  
      public static @Nullable String removePrefix(final @Nullable String input, final @NonNull String prefix) {
          if(input==null) {
              return null;
          }
          return input.startsWith(prefix)
                  ? input.substring(prefix.length())
                  : input;
      }
  
      /**
       * Returns a {@link StringOperator} that converts given literal into {@code prefix + literal + suffix} ,
       * unless the literal is {@code null}, in which case the operator returns {@code null}.
       */
      public static final StringOperator bracketed(final @NonNull String prefix, final @NonNull String suffix) {
          return s->s!=null
                  ? prefix + s + suffix
                  : null;
      }
  
      // -- REDUCTION (BINARY OPERATIOR)
  
      /**
       * Combines 2 strings {@code left} and {@code right} into a single string, such that left
       * and right are delimited by the {@code delimiter} and such that
       * the result does not introduce a sequence of delimiters, like for example when building file-system
       * paths from chunks.
       *
       * @param left
       * @param right
       * @param delimiter
       * @return non-null
       */
      public static String combineWithDelimiter(
              final @Nullable String left, final @Nullable String right, final String delimiter) {
  
          requireNonEmpty(delimiter, "pathDelimiter");
  
          if (isNullOrEmpty(left) && isNullOrEmpty(right)) {
              return "";
          }
          if (isNullOrEmpty(left)) {
              return right;
          }
          if (isNullOrEmpty(right)) {
              return left;
          }
          if (left.endsWith(delimiter) || right.startsWith(delimiter)) {
              return left + right;
          }
          return left + delimiter + right;
      }
  
      // -- PADDING
  
      /**
       * Returns a string, of length at least minLength, consisting of string prepended with as many copies
       * of padChar as are necessary to reach that length.
       * @param str
       * @param minLength
       * @param c
       */
      public static String padStart(final @Nullable String str, final int minLength, final char c) {
          if(minLength<=0) {
              return str;
          }
          final int len = size(str);
          if(len>=minLength) {
              return str;
          }
  
          final int fillCount = minLength - len;
  
          return of(fillCount, c) + nullToEmpty(str);
      }
  
      /**
       * Returns a string, of length at least minLength, consisting of string appended with as many copies
       * of padChar as are necessary to reach that length.
       * @param str
       * @param minLength
       * @param c
       */
      public static String padEnd(final @Nullable String str, final int minLength, final char c) {
          if(minLength<=0) {
              return str;
          }
          final int len = size(str);
          if(len>=minLength) {
              return str;
          }
  
          final int fillCount = minLength - len;
  
          return nullToEmpty(str) + of(fillCount, c);
      }
  
      /**
       * Returns a string that is a substring of given {@code str}.
       * The substring begins at the specified beginIndex and extends to the character at index endIndex - 1.
       * Thus the length of the substring is endIndex-beginIndex.
       * <p>
       * Supports negative {@code endIndex}, as well as index overflow.
       * If the endIndex is negative, it is understood as being relative to the end of the given {@code str}.
       */
      public static String substring(final @Nullable String str, final int beginIndex, final int endIndex) {
          if(isEmpty(str)) {
              return str;
          }
          final int maxIndex = str.length()-1; // >= 0
  
          final int i0 = beginIndex>0
                  ? Math.min(beginIndex, maxIndex)
                  : 0;
  
          final int i1 = Math.min(
                  maxIndex+1,
                  endIndex<0
                      ? str.length() + endIndex
                      : endIndex
                  );
  
          return i0<i1
                  ? str.substring(i0, i1)
                  : "";
      }
  
      // -- SPLITTING
  
      /**
       * Splits the {@code input} into chunks separated by {@code separator},
       * then puts all chunks on the returned stream.
       * <p>
       * Corner cases:
       * <ul>
       * <li>{@code input} starts with {@code separator}: an empty string is the first chunk put on the stream</li>
       * <li>{@code input} ends with {@code separator}: an empty string is the last chunk put on the stream</li>
       * <li>a {@code separator} is followed by another: an empty string is put on the stream</li>
       * </ul>
       * @param input
       * @param separator non-empty string
       * @return empty stream if {@code input} is null
       * @throws IllegalArgumentException if {@code separator} is empty
       */
      public static Stream<String> splitThenStream(final @Nullable String input, final String separator) {
          if(isEmpty(separator)) {
              throw new IllegalArgumentException("a non empty separator is required");
          }
          if(isEmpty(input)) {
              return Stream.of();
          }
          if(!input.contains(separator)) {
              return Stream.of(input);
          }
  
          return StreamSupport.stream(
                  Spliterators.spliteratorUnknownSize(splitIterator(input, separator), Spliterator.ORDERED),
                  false); // not parallel
      }
  
      /**
       * Creates a stream from the given input sequence around matches of {@code delimiterPattern}.
       * @param input
       * @param delimiterPattern
       */
      public static Stream<String> splitThenStream(final @Nullable CharSequence input, final @NonNull Pattern delimiterPattern) {
          if(isEmpty(input)) {
              return Stream.of();
          }
          return delimiterPattern.splitAsStream(input);
      }
  
      /**
       * Optionally applies {@code onNonEmptySplit} function, based on whether both split parts
       * <i>lhs</i> and <i>rhs</i> are non empty Strings.
       * @param <T>
       * @param input
       * @param separator
       * @param onNonEmptySplit
       */
      public static <T> Optional<T> splitThenApplyRequireNonEmpty(
              final @Nullable String input,
              final String separator,
              final BiFunction<String, String, T> onNonEmptySplit) {
  
          if(isEmpty(input)) {
              return Optional.empty();
          }
          // we have a non-empty string
          final int p = input.indexOf(separator);
          if(p<1){
              // separator not found or
              // empty lhs in string
              return Optional.empty();
          }
          final int q = p + separator.length();
          if(q==input.length()) {
              // empty rhs
              return Optional.empty();
          }
          return Optional.ofNullable(onNonEmptySplit.apply(input.substring(0, p), input.substring(q)));
      }
  
      public static void splitThenAccept(
              final @Nullable String input,
              final String separator,
              final BiConsumer<String, String> onNonEmptySplit,
              final Consumer<String> onNonEmptyLhs,
              final Consumer<String> onNonEmptyRhs) {
  
          _Strings_FastSplit.splitThenAccept(input, separator, onNonEmptySplit, onNonEmptyLhs, onNonEmptyRhs);
      }
  
      public static void splitThenAcceptEmptyAsNull(
              final @Nullable String input,
              final String separator,
              final BiConsumer<String, String> onSplit) {
  
          _Strings_FastSplit.splitThenAccept(input, separator, onSplit,
                  lhs->onSplit.accept(lhs, null),
                  rhs->onSplit.accept(null, rhs));
      }
  
      public static Stream<String> grep(final @Nullable String input, @Nullable Predicate<String> matcher){
          matcher = matcher!=null ? matcher : _Predicates.alwaysTrue();
          return splitThenStream(input, "\n")
                  .filter(_Strings::isNotEmpty)
                  .filter(matcher)
                  .map(s->s.replace("\r", ""));
      }
  
      public static Stream<String> grep(final @Nullable String input, final @Nullable String contains){
          final Predicate<String> matcher = contains!=null ? line->line.contains(contains) : _Predicates.alwaysTrue();
          return grep(input, matcher);
      }
  
      // -- REPLACEMENT OPERATORS
  
      /**
       * Condenses any whitespace to the given {@code replacement}
       *
       * @param input
       * @param replacement
       * @return null if {@code input} is null
       */
      public static String condenseWhitespaces(final @Nullable String input, final @NonNull String replacement) {
          return mapIfPresentElseNull(input, __->input.replaceAll("\\s+", replacement));
      }
  
      /**
       * ...xyz
       * @param input
       * @param maxLength
       * @param ellipsis
       * @return (non-null), ellipsified version of {@code input}, if {@code input} exceeds length {@code maxLength}
       */
      public static String ellipsifyAtStart(
              final @Nullable CharSequence input,
              final int maxLength,
              final @NonNull CharSequence ellipsis) {
  
          if(input==null) {
              return "";
          }
          if(input.length()<=maxLength) {
              return input.toString();
          }
          final int trimmedLength = maxLength - ellipsis.length();
          final int end = input.length();
          final int start = end - trimmedLength;
          return String.join("", ellipsis, input.subSequence(start, end));
      }
  
      /**
       * abc...
       * @param input
       * @param maxLength
       * @param ellipsis
       * @return (non-null), ellipsified version of {@code input}, if {@code input} exceeds length {@code maxLength}
       */
      public static String ellipsifyAtEnd(
              final @Nullable CharSequence input,
              final int maxLength,
              final @NonNull CharSequence ellipsis) {
  
          if(input==null) {
              return "";
          }
          if(input.length()<=maxLength) {
              return input.toString();
          }
          final int trimmedLength = maxLength - ellipsis.length();
          return String.join("", input.subSequence(0, trimmedLength), ellipsis);
      }
  
      // -- READ FROM INPUT STREAM
  
      public static String read(final @Nullable InputStream input, final @NonNull Charset charset) {
          if(input==null) {
              return "";
          }
          // see https://stackoverflow.com/questions/309424/how-to-read-convert-an-inputstream-into-a-string-in-java
          try(Scanner scanner = new Scanner(input, charset.name())){
              scanner.useDelimiter("\\A");
              return scanner.hasNext() ? scanner.next() : "";
          }
      }
  
      @SneakyThrows
      public static String readFromResource(
              final @NonNull Class<?> resourceLocation,
              final @NonNull String resourceName,
              final @NonNull Charset charset) {
          try(InputStream input = resourceLocation.getResourceAsStream(resourceName)){
              return read(input, charset);
          }
      }
  
      // -- PRINTING
  
      /**
       * Returns the {@link String} as printed into by the given {@code printer}
       * @param printer - consumes the generated PrintStream to print to
       * @param charset
       * @see PrintStream
       * @see Charset
       */
      @SneakyThrows
      public static String print(
              final @NonNull Consumer<PrintStream> printer,
              final @NonNull Charset charset) {
          var baos = new ByteArrayOutputStream();
          try (PrintStream ps = new PrintStream(baos, true, charset)) {
              printer.accept(ps);
          }
          return baos.toString(charset);
      }
  
      /**
       * Shortcut to {@link #print(Consumer, Charset)} using {@code UTF8}.
       */
      public static String printUtf8(
              final @NonNull Consumer<PrintStream> printer) {
          return print(printer, StandardCharsets.UTF_8);
      }
  
      // -- BYTE ARRAY CONVERSION
  
      /**
       * Encodes {@code str} into a sequence of bytes using the given {@code charset}.
       * @param str
       * @param charset
       * @return null if {@code str} is null
       */
      public static final byte[] toBytes(final @Nullable String str, final @NonNull Charset charset) {
          return Optional.ofNullable(str)
                  .map(s->s.getBytes(charset))
                  .orElse(null);
      }
  
      /**
       * Constructs a new String by decoding the specified array of bytes using the specified {@code charset}.
       * @param bytes
       * @param charset
       * @return null if {@code bytes} is null
       */
      public static final String ofBytes(final @Nullable byte[] bytes, final @NonNull Charset charset) {
          return Optional.ofNullable(bytes)
                  .map(b->new String(b, charset))
                  .orElse(null);
      }
  
      /**
       * Factory counterpart to {@link String#codePoints()}.
       * @return null if {@code codePoints} is null
       */
      public static final String ofCodePoints(final @Nullable IntStream codePoints) {
          if(codePoints==null) {
              return null;
          }
          return codePoints.collect(
                  StringBuilder::new,
                  StringBuilder::appendCodePoint,
                  StringBuilder::append)
              .toString();
      }
  
      /**
       * Converts the {@code input} to a byte array using the specified {@code charset},
       * then applies the byte manipulation operator {@code converter},
       * then converts the (manipulated) byte array back to a string, again using the specified {@code charset}.
       * @param input
       * @param converter
       * @param charset
       * @return null if {@code input} is null
       */
      public static final String convert(final @Nullable String input, final @NonNull BytesOperator converter, final @NonNull Charset charset) {
          return mapIfPresentElseNull(input, __->ofBytes(converter.apply(toBytes(input, charset)), charset));
      }
  
      // -- UNARY OPERATOR COMPOSITION
  
      /**
       * Helper for composing of {@code UnaryOperator<String>}.
       */
      @FunctionalInterface
      public static interface StringOperator extends UnaryOperator<String> {
          default StringOperator compose(final UnaryOperator<String> andThen) {
              return str->this.andThen(andThen).apply(str);
          }
          /**
           * Returns a unary operator that always returns its input argument.
           */
          static StringOperator identity() {
              return s -> s;
          }
      }
  
      // -- SPECIAL COMPOSITES
  
      // using naming convention asXxx...
  
      public static final StringOperator asLowerCase = _Strings::lower;
      public static final StringOperator asUpperCase = _Strings::upper;
      public static final StringOperator asCapitalized = _Strings::capitalize;
      public static final StringOperator asDecapitalized = _Strings::decapitalize;
  
      /**
       * Converts given literal into a double-quoted literal, unless the literal is {@code null},
       * in which case {@code null} is returned.
       */
      public static final StringOperator asDoubleQuoted = bracketed("\"", "\"");
      /**
       * Converts given literal into a square-bracketed literal, unless the literal is {@code null},
       * in which case {@code null} is returned.
       */
      public static final StringOperator asSquareBracketed = bracketed("[", "]");
      /**
       * Converts given literal into a parenthesis-bracketed literal, unless the literal is {@code null},
       * in which case {@code null} is returned.
       */
      public static final StringOperator asParenthesisBracketed = bracketed("(", ")");
      /**
       * Converts given literal into a curly-bracketed literal, unless the literal is {@code null},
       * in which case {@code null} is returned.
       */
      public static final StringOperator asCurlyBracketed = bracketed("{", "}");
  
      public static final StringOperator asLowerDashed = asLowerCase
              .compose(s->_Strings.condenseWhitespaces(s, "-"));
  
      public static final StringOperator asNormalized =
              s->_Strings.condenseWhitespaces(s, " ");
  
      /**
       * Returns a word spaced version of the specified name, so there are spaces
       * between the words, where each word starts with a capital letter. E.g.,
       * "NextAvailableDate" is returned as "Next Available Date".
       */
      public static final StringOperator asNaturalName =
              s->_Strings_NaturalName.naturalName(s, true);
  
      /**
       * Camel case is the practice of writing phrases without spaces or punctuation and with capitalized words.
       * The format indicates the first word starting with EITHER case,
       * then the following words having an initial uppercase letter.
       */
      public static final StringOperator asCamelCase =
              s->_Strings_CamelCase.camelCase(s, firstToken->firstToken);
  
      public static final StringOperator asCamelCaseDecapitalized =
              s->_Strings_CamelCase.camelCase(s, asDecapitalized);
  
      public static final StringOperator asCamelCaseCapitalized =
              s->_Strings_CamelCase.camelCase(s, asCapitalized);
      public static final StringOperator asPascalCase = asCamelCaseCapitalized; // synonym
  
      public static final String asFileNameWithExtension(final @NonNull String fileName, final @NonNull String fileExtension) {
          return suffix(fileName, prefix(fileExtension, "."));
      }
  
      /**
       * Checks whether given fileName is already extended by any of the proposedFileExtensions.
       * If not, appends the first proposedFileExtension, otherwise acts as identity operation.
       */
      public static final String asFileNameWithExtension(
              final @NonNull String fileName,
              final @NonNull Can<String> proposedFileExtensions) {
  
          switch(proposedFileExtensions.getCardinality()) {
          case ZERO: return fileName;
          case ONE: return asFileNameWithExtension(fileName, proposedFileExtensions.getFirst().orElse(""));
          case MULTIPLE:
              break; // fall through
          }
  
          var fileNameLower = fileName.toLowerCase();
          var isAlreadyExended = proposedFileExtensions.stream()
              .map(ext->ext.toLowerCase())
              .anyMatch(ext->fileNameLower.endsWith("." + ext));
  
          return isAlreadyExended
                  ? fileName
                  : asFileNameWithExtension(fileName, proposedFileExtensions.getFirst().orElse(""));
      }
  
      /**
       * Returns the name of a Java entity without any prefix. A prefix is defined
       * as the first set of lower-case letters and the name is characters from,
       * and including, the first upper case letter. If no upper case letter is
       * found then an empty string is returned.
       *
       * <p>
       * Calling this method with the following Java names will produce these
       * results:
       *
       * <pre>
       * getCarRegistration -&gt; CarRegistration
       * CityMayor          -&gt; CityMayor
       * isReady            -&gt; Ready
       * </pre>
       *
       */
      public static String baseName(final @NonNull String methodName) {
          var asPrefixDropped = isNotEmpty(methodName)
                  ? ofCodePoints(
                          methodName.codePoints()
                              .dropWhile(c->c != '_' && Character.isLowerCase(c)))
                  : "";
          var baseName = asPrefixDropped.isEmpty()
                  ? methodName
                  : asPrefixDropped;
          var javaBaseName = _Strings.capitalize(baseName.trim());
          _Assert.assertNotEmpty(javaBaseName,
                  ()->String.format("framework bug: could not create a base name from '%s'", methodName));
          return javaBaseName;
      }
  
      /**
       * Within given string, converts any special UTF-8 variants of the space ' ' character to the regular one.
       */
      public static final String asRegularSpaces(final @Nullable CharSequence chars) {
          return isNotEmpty(chars)
                  ? ofCodePoints(
                          chars.codePoints()
                              .map(_Strings::toRegularSpaceCharacter))
                  : chars!=null ? "" : null;
      }
  
      // -- SHORTCUTS
  
      /**
       * Like {@link _Strings#splitThenStream(String, String)} but also trimming each junk, then discarding
       * empty chunks.
       * @return empty stream if {@code input} is null
       */
      public static Stream<String> splitThenStreamTrimmed(final @Nullable String input, final String separator) {
          return splitThenStream(input, separator)
                  .map(String::trim)
                  .filter(not(String::isEmpty));
      }
  
      /**
       * Like {@link _Strings#splitThenStream(CharSequence, Pattern)} but also trimming each junk,
       * then discarding empty chunks.
       * @return empty stream if {@code input} is null
       */
      public static Stream<String> splitThenStreamTrimmed(final @Nullable CharSequence input, final Pattern delimiterPattern) {
          return splitThenStream(input, delimiterPattern)
                  .map(String::trim)
                  .filter(not(String::isEmpty));
      }
  
      public static String base64UrlDecode(final @Nullable String str) {
          return _Strings.convert(str, _Bytes.ofUrlBase64, StandardCharsets.UTF_8);
      }
  
      public static String base64UrlEncode(final @Nullable String str) {
          return _Strings.convert(str, _Bytes.asUrlBase64, StandardCharsets.UTF_8);
      }
  
      public static String base64UrlDecodeZlibCompressed(final @Nullable String str) {
          return _Strings.convert(str, _Bytes.ofZlibCompressedUrlBase64, StandardCharsets.UTF_8);
      }
  
      public static String base64UrlEncodeZlibCompressed(final @Nullable String str) {
          return _Strings.convert(str, _Bytes.asZlibCompressedUrlBase64, StandardCharsets.UTF_8);
      }
  
      // -- CHARACTER PROCESSING
  
      /**
       * Converts any special UTF-8 variants of the space ' ' character to the regular one.
       */
      public static int toRegularSpaceCharacter(final int codePoint) {
          // NO-BREAK SPACE (UTF-8/160)
          // THIN SPACE (UTF-8/8201)
          // NARROW NO-BREAK SPACE (UTF-8/8239)
          // REGULAR SPACE (UTF-8/32)
          switch (codePoint) {
          case 160:
          case 8201:
          case 8239:
              return 32;
          default:
              return codePoint;
          }
      }
  
      // -- TRUNCATION
  
      public static String trimmed(final String str, final int lengthOfField) {
          if (str == null) {
              return null;
          }
          if (str.length() > lengthOfField) {
              return str.substring(0, lengthOfField - 3) + "...";
          }
          return str;
      }
  
      /**
       * for example, so that a DB type converter can return null if the string wouldn't fit into a target column.
       */
      public static String nullIfExceeds(final String str, final int maxLength) {
          return str == null || str.length() > maxLength
                      ? null
                      : str;
      }
  
      // -- HELPER
  
      /**
       * Equivalent to {@code Optional.ofNullable(obj).map(mapper).orElse(null);}
       */
      private static String mapIfPresentElseNull(
              final @Nullable String obj, final @NonNull UnaryOperator<String> mapper) {
          return obj!=null
                  ? mapper.apply(obj)
                  : null;
      }
  
  }
  