001/*
002 * Copyright (C) 2006 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the License
010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
011 * or implied. See the License for the specific language governing permissions and limitations under
012 * the License.
013 */
014
015package com.google.common.escape;
016
017import static com.google.common.base.Preconditions.checkNotNull;
018
019import com.google.common.annotations.Beta;
020import com.google.common.annotations.GwtCompatible;
021import com.google.errorprone.annotations.CanIgnoreReturnValue;
022import java.util.HashMap;
023import java.util.Map;
024import java.util.Map.Entry;
025import javax.annotation.CheckForNull;
026import org.checkerframework.checker.nullness.qual.Nullable;
027
028/**
029 * Simple helper class to build a "sparse" array of objects based on the indexes that were added to
030 * it. The array will be from 0 to the maximum index given. All non-set indexes will contain null
031 * (so it's not really a sparse array, just a pseudo sparse array). The builder can also return a
032 * CharEscaper based on the generated array.
033 *
034 * @author Sven Mawson
035 * @since 15.0
036 */
037@Beta
038@GwtCompatible
039@ElementTypesAreNonnullByDefault
040public final class CharEscaperBuilder {
041  /**
042   * Simple decorator that turns an array of replacement char[]s into a CharEscaper, this results in
043   * a very fast escape method.
044   */
045  private static class CharArrayDecorator extends CharEscaper {
046    private final char[] @Nullable [] replacements;
047    private final int replaceLength;
048
049    CharArrayDecorator(char[] @Nullable [] replacements) {
050      this.replacements = replacements;
051      this.replaceLength = replacements.length;
052    }
053
054    /*
055     * Overriding escape method to be slightly faster for this decorator. We test the replacements
056     * array directly, saving a method call.
057     */
058    @Override
059    public String escape(String s) {
060      int slen = s.length();
061      for (int index = 0; index < slen; index++) {
062        char c = s.charAt(index);
063        if (c < replacements.length && replacements[c] != null) {
064          return escapeSlow(s, index);
065        }
066      }
067      return s;
068    }
069
070    @Override
071    @CheckForNull
072    protected char[] escape(char c) {
073      return c < replaceLength ? replacements[c] : null;
074    }
075  }
076
077  // Replacement mappings.
078  private final Map<Character, String> map;
079
080  // The highest index we've seen so far.
081  private int max = -1;
082
083  /** Construct a new sparse array builder. */
084  public CharEscaperBuilder() {
085    this.map = new HashMap<>();
086  }
087
088  /** Add a new mapping from an index to an object to the escaping. */
089  @CanIgnoreReturnValue
090  public CharEscaperBuilder addEscape(char c, String r) {
091    map.put(c, checkNotNull(r));
092    if (c > max) {
093      max = c;
094    }
095    return this;
096  }
097
098  /** Add multiple mappings at once for a particular index. */
099  @CanIgnoreReturnValue
100  public CharEscaperBuilder addEscapes(char[] cs, String r) {
101    checkNotNull(r);
102    for (char c : cs) {
103      addEscape(c, r);
104    }
105    return this;
106  }
107
108  /**
109   * Convert this builder into an array of char[]s where the maximum index is the value of the
110   * highest character that has been seen. The array will be sparse in the sense that any unseen
111   * index will default to null.
112   *
113   * @return a "sparse" array that holds the replacement mappings.
114   */
115  public char[] @Nullable [] toArray() {
116    char[][] result = new char[max + 1][];
117    for (Entry<Character, String> entry : map.entrySet()) {
118      result[entry.getKey()] = entry.getValue().toCharArray();
119    }
120    return result;
121  }
122
123  /**
124   * Convert this builder into a char escaper which is just a decorator around the underlying array
125   * of replacement char[]s.
126   *
127   * @return an escaper that escapes based on the underlying array.
128   */
129  public Escaper toEscaper() {
130    return new CharArrayDecorator(toArray());
131  }
132}