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 *      https://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 */
017
018package org.apache.commons.text.translate;
019
020import java.io.IOException;
021import java.io.StringWriter;
022import java.io.UncheckedIOException;
023import java.io.Writer;
024import java.util.Locale;
025
026import org.apache.commons.lang3.Validate;
027
028/**
029 * An API for translating text. Its core use is to escape and unescape text. Because escaping and unescaping is completely contextual, the API does not present
030 * two separate signatures.
031 *
032 * @since 1.0
033 */
034public abstract class CharSequenceTranslator {
035
036    /**
037     * Array containing the hexadecimal alphabet.
038     */
039    static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
040
041    /**
042     * Returns an upper case hexadecimal {@code String} for the given character.
043     *
044     * @param codePoint The code point to convert.
045     * @return An upper case hexadecimal {@code String}
046     */
047    public static String hex(final int codePoint) {
048        return Integer.toHexString(codePoint).toUpperCase(Locale.ENGLISH);
049    }
050
051    /**
052     * Construct a new instance.
053     */
054    public CharSequenceTranslator() {
055        // empty
056    }
057
058    /**
059     * Translates an input. This is intentionally final as its algorithm is tightly coupled with the abstract method of this class.
060     *
061     * @param input CharSequence to be translated.
062     * @return String output of translation.
063     */
064    public final String translate(final CharSequence input) {
065        if (input == null) {
066            return null;
067        }
068        try {
069            final StringWriter writer = new StringWriter(input.length() * 2);
070            translate(input, writer);
071            return writer.toString();
072        } catch (final IOException ioe) {
073            // this should never ever happen while writing to a StringWriter
074            throw new UncheckedIOException(ioe);
075        }
076    }
077
078    /**
079     * Translates a set of code points, represented by an int index into a CharSequence, into another set of code points. The number of code points consumed
080     * must be returned, and the only IOExceptions thrown must be from interacting with the Writer so that the top level API may reliably ignore StringWriter
081     * IOExceptions.
082     *
083     * @param input  CharSequence that is being translated.
084     * @param index  int representing the current point of translation.
085     * @param writer Writer to translate the text to.
086     * @return int count of code points consumed.
087     * @throws IOException Thrown if and only if the Writer produces an IOException.
088     */
089    public abstract int translate(CharSequence input, int index, Writer writer) throws IOException;
090
091    /**
092     * Translates an input onto a Writer. This is intentionally final as its algorithm is tightly coupled with the abstract method of this class.
093     *
094     * @param input  CharSequence that is being translated.
095     * @param writer Writer to translate the text to.
096     * @throws IOException Thrown if and only if the Writer produces an IOException.
097     */
098    public final void translate(final CharSequence input, final Writer writer) throws IOException {
099        Validate.isTrue(writer != null, "The Writer must not be null");
100        if (input == null) {
101            return;
102        }
103        int pos = 0;
104        final int len = input.length();
105        while (pos < len) {
106            final int consumed = translate(input, pos, writer);
107            if (consumed == 0) {
108                // inlined implementation of Character.toChars(Character.codePointAt(input, pos))
109                // avoids allocating temp char arrays and duplicate checks
110                final char c1 = input.charAt(pos);
111                writer.write(c1);
112                pos++;
113                if (Character.isHighSurrogate(c1) && pos < len) {
114                    final char c2 = input.charAt(pos);
115                    if (Character.isLowSurrogate(c2)) {
116                        writer.write(c2);
117                        pos++;
118                    }
119                }
120                continue;
121            }
122            // contract with translators is that they have to understand code points
123            // and they just took care of a surrogate pair
124            for (int pt = 0; pt < consumed; pt++) {
125                pos += Character.charCount(Character.codePointAt(input, pos));
126            }
127        }
128    }
129
130    /**
131     * Creates a merger of this translator with another set of translators. Useful in customizing the standard functionality.
132     *
133     * @param translators CharSequenceTranslator array of translators to merge with this one.
134     * @return CharSequenceTranslator merging this translator with the others.
135     */
136    public final CharSequenceTranslator with(final CharSequenceTranslator... translators) {
137        final CharSequenceTranslator[] newArray = new CharSequenceTranslator[translators.length + 1];
138        newArray[0] = this;
139        System.arraycopy(translators, 0, newArray, 1, translators.length);
140        return new AggregateTranslator(newArray);
141    }
142}