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}