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.beam.sdk.util;
  
  import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull;
  
  import java.util.ArrayList;
  import java.util.List;
  import org.apache.beam.sdk.annotations.Internal;
  import org.checkerframework.checker.nullness.qual.Nullable;
  
  /** Utilities for working with JSON and other human-readable string formats. */
  @Internal
  public class StringUtils {
    /**
     * Converts the given array of bytes into a legal JSON string.
     *
     * <p>Uses a simple strategy of converting each byte to a single char, except for non-printable
     * chars, non-ASCII chars, and '%', '\', and '"', which are encoded as three chars in '%xx'
     * format, where 'xx' is the hexadecimal encoding of the byte.
     */
    public static String byteArrayToJsonString(byte[] bytes) {
      StringBuilder sb = new StringBuilder(bytes.length * 2);
      for (byte b : bytes) {
        if (b >= 32 && b < 127) {
          // A printable ascii character.
          char c = (char) b;
          if (c != '%' && c != '\\' && c != '\"') {
            // Not an escape prefix or special character, either.
            // Send through unchanged.
            sb.append(c);
            continue;
          }
        }
        // Send through escaped.  Use '%xx' format.
        sb.append(String.format("%%%02x", b));
      }
      return sb.toString();
    }
  
    /**
     * Converts the given string, encoded using {@link #byteArrayToJsonString}, into a byte array.
     *
     * @throws IllegalArgumentException if the argument string is not legal
     */
    public static byte[] jsonStringToByteArray(String string) {
      List<Byte> bytes = new ArrayList<>();
      for (int i = 0; i < string.length(); ) {
        char c = string.charAt(i);
        Byte b;
        if (c == '%') {
          // Escaped.  Expect '%xx' format.
          try {
            b = (byte) Integer.parseInt(string.substring(i + 1, i + 3), 16);
          } catch (IndexOutOfBoundsException | NumberFormatException exn) {
            throw new IllegalArgumentException(
                "not in legal encoded format; "
                    + "substring ["
                    + i
                    + ".."
                    + (i + 2)
                    + "] not in format \"%xx\"",
                exn);
          }
          i += 3;
        } else {
          // Send through unchanged.
          b = (byte) c;
          i++;
        }
        bytes.add(b);
      }
      byte[] byteArray = new byte[bytes.size()];
      int i = 0;
      for (Byte b : bytes) {
        byteArray[i++] = b;
      }
      return byteArray;
    }
  
    /**
     * Calculate the Levenshtein distance between two strings.
     *
     * <p>The Levenshtein distance between two words is the minimum number of single-character edits
     * (i.e. insertions, deletions or substitutions) required to change one string into the other.
     */
    public static int getLevenshteinDistance(final String s, final String t) {
      checkNotNull(s);
      checkNotNull(t);
  
      // base cases
      if (s.equals(t)) {
        return 0;
      }
      if (s.length() == 0) {
        return t.length();
      }
      if (t.length() == 0) {
        return s.length();
      }
  
      // create two work arrays to store integer distances
      final int[] v0 = new int[t.length() + 1];
      final int[] v1 = new int[t.length() + 1];
  
      // initialize v0 (the previous row of distances)
      // this row is A[0][i]: edit distance for an empty s
      // the distance is just the number of characters to delete from t
      for (int i = 0; i < v0.length; i++) {
        v0[i] = i;
      }
  
      for (int i = 0; i < s.length(); i++) {
        // calculate v1 (current row distances) from the previous row v0
  
        // first element of v1 is A[i+1][0]
        //   edit distance is delete (i+1) chars from s to match empty t
        v1[0] = i + 1;
  
        // use formula to fill in the rest of the row
        for (int j = 0; j < t.length(); j++) {
          int cost = (s.charAt(i) == t.charAt(j)) ? 0 : 1;
          v1[j + 1] = Math.min(Math.min(v1[j] + 1, v0[j + 1] + 1), v0[j] + cost);
        }
  
        // copy v1 (current row) to v0 (previous row) for next iteration
        System.arraycopy(v1, 0, v0, 0, v0.length);
      }
  
      return v1[t.length()];
    }
  
    /**
     * Convert Array to new lined String. Truncate to first {@code maxLine} elements.
     *
     * <p>Useful to truncate stacktrace and for logging.
     */
    public static String arrayToNewlines(Object[] array, int maxLine) {
      int n = (maxLine > 0 && array.length > maxLine) ? maxLine : array.length;
      StringBuilder b = new StringBuilder();
      for (int i = 0; i < n; i++) {
        b.append(array[i]);
        b.append("\n");
      }
      if (array.length > maxLine) {
        b.append("...\n");
      }
      return b.toString();
    }
  
    /**
     * Truncate String if length greater than maxLen, and append "..." to the end. Handles null.
     *
     * <p>Useful to truncate long logging message.
     */
    public static String leftTruncate(@Nullable Object element, int maxLen) {
      if (element == null) {
        return "";
      }
      String s = element.toString();
      if (s.length() > maxLen) {
        return s.substring(0, maxLen) + "...";
      }
      return s;
    }
  }
  