TransliteratingASCIIProvider.java
package com.maybeitssquid.safeascii;
import com.maybeitssquid.safeascii.internal.ASCIIFilter;
import com.maybeitssquid.safeascii.internal.Cache;
import com.maybeitssquid.safeascii.internal.Decompose;
import com.maybeitssquid.safeascii.internal.Name;
import com.maybeitssquid.safeascii.internal.SingleCharacterFilter;
import java.nio.charset.Charset;
import java.nio.charset.spi.CharsetProvider;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.function.IntFunction;
/**
* A {@link CharsetProvider} that supplies ASCII-safe character sets for encoding Unicode text. This
* provider offers five distinct character set implementations:
*
* <dl>
* <dt>X-ASCII-Printable (alias: ASCII-Printable)
* <dd>Strict printable ASCII: allows 0x20 through 0x7E inclusive. Control characters, including
* tabs and newlines, are reported as unmappable.
* <dt>X-ASCII-Plain (alias: ASCII-Plain)
* <dd>Printable ASCII with newline support: linefeed (0x0A) passes through; carriage return
* (0x0D) is unmappable so CRLF normalises to LF under the {@code IGNORE} error action. All
* other control characters are unmappable.
* <dt>X-ASCII-Formatted (alias: ASCII-Formatted)
* <dd>Printable ASCII with tab and newline support: tab (0x09) and linefeed (0x0A) pass through;
* carriage return (0x0D) is unmappable so CRLF normalises to LF under the {@code IGNORE}
* error action. All other control characters are unmappable.
* <dt>X-Transliterating
* <dd>Aggressive Unicode-to-ASCII transliteration using NFKD decomposition and character-name
* lookup. Output length may vary (one Unicode character may produce multiple ASCII bytes).
* <dt>X-Transliterating-Single-Byte (alias: ACH)
* <dd>Same transliteration as X-Transliterating but guarantees 1:1 character output. Any
* transliteration that would produce more or fewer than one character is rejected as
* unmappable.
* </dl>
*/
public class TransliteratingASCIIProvider extends CharsetProvider {
/** Canonical name for the strict printable ASCII character set. */
public static final String ASCII_PRINTABLE_CHARSET = "X-ASCII-Printable";
/** Alias for {@link #ASCII_PRINTABLE_CHARSET}. */
public static final String ASCII_PRINTABLE_ALIAS = "ASCII-Printable";
/** Canonical name for the printable ASCII character set with newline support. */
public static final String ASCII_PLAIN_CHARSET = "X-ASCII-Plain";
/** Alias for {@link #ASCII_PLAIN_CHARSET}. */
public static final String ASCII_PLAIN_ALIAS = "ASCII-Plain";
/** Canonical name for the printable ASCII character set with tab and newline support. */
public static final String ASCII_FORMATTED_CHARSET = "X-ASCII-Formatted";
/** Alias for {@link #ASCII_FORMATTED_CHARSET}. */
public static final String ASCII_FORMATTED_ALIAS = "ASCII-Formatted";
/** Canonical name for the transliterating character set. */
public static final String TRANSLITERATING_CHARSET = "X-Transliterating";
/** Canonical name for the single-byte transliterating character set. */
public static final String TRANSLITERATING_SINGLE_BYTE_CHARSET = "X-Transliterating-Single-Byte";
/** ACH alias for the single-byte transliterating character set. */
public static final String ACH_ALIAS = "ACH";
private static final List<Charset> CHARSETS =
List.of(
asciiPrintable(),
asciiPlain(),
asciiFormatted(),
transliterating(),
transliteratingSingleByte());
/**
* Creates a new TransliteratingASCIIProvider instance.
*
* <p>The available character sets are initialized once and safely shared by every provider
* instance.
*/
public TransliteratingASCIIProvider() {
// Default constructor
}
private static Charset asciiPrintable() {
final ASCIIFilter filter = new ASCIIFilter(Character.CONTROL);
final Cache transliterator = new Cache(filter);
return new TransliteratingASCII(transliterator, ASCII_PRINTABLE_CHARSET, ASCII_PRINTABLE_ALIAS);
}
private static Charset asciiPlain() {
final ASCIIFilter filter = new ASCIIFilter(Character.CONTROL);
final Cache cache = new Cache(filter);
cache.cache(0x0A, "\n");
cache.cache(0x0D, ""); // CR is unmappable; CRLF normalises to LF under IGNORE
return new TransliteratingASCII(cache, ASCII_PLAIN_CHARSET, ASCII_PLAIN_ALIAS);
}
private static Charset asciiFormatted() {
final ASCIIFilter filter = new ASCIIFilter(Character.CONTROL);
final Cache cache = new Cache(filter);
cache.cache(0x09, "\t");
cache.cache(0x0A, "\n");
cache.cache(0x0D, ""); // CR is unmappable; CRLF normalises to LF under IGNORE
return new TransliteratingASCII(cache, ASCII_FORMATTED_CHARSET, ASCII_FORMATTED_ALIAS);
}
private static Charset transliterating() {
final ASCIIFilter filter = new ASCIIFilter();
final IntFunction<CharSequence> transliterator = new Decompose(new Name(filter));
final Cache cache = new Cache(transliterator);
return new TransliteratingASCII(cache, TRANSLITERATING_CHARSET);
}
private static Charset transliteratingSingleByte() {
final ASCIIFilter filter = new ASCIIFilter();
final IntFunction<CharSequence> transliterator = new Decompose(new Name(filter));
final SingleCharacterFilter lengthPreserving = new SingleCharacterFilter(transliterator);
final Cache cache = new Cache(lengthPreserving);
return new TransliteratingASCII(cache, TRANSLITERATING_SINGLE_BYTE_CHARSET, ACH_ALIAS);
}
/**
* Returns an iterator over all available character sets provided by this class. The available
* charsets are: ASCII-Printable, ASCII-Plain, ASCII-Formatted, X-Transliterating, and
* X-Transliterating-Single-Byte.
*
* @return Iterator containing all supported character sets {@code @ThreadSafe} This method is
* thread-safe and uses lazy initialization
*/
@Override
public Iterator<Charset> charsets() {
return CHARSETS.iterator();
}
/**
* Retrieves a specific character set by name. Supported charset names are:
*
* <ul>
* <li>{@link #ASCII_PRINTABLE_CHARSET X-ASCII-Printable} (alias: {@link #ASCII_PRINTABLE_ALIAS
* ASCII-Printable})
* <li>{@link #ASCII_PLAIN_CHARSET X-ASCII-Plain} (alias: {@link #ASCII_PLAIN_ALIAS
* ASCII-Plain})
* <li>{@link #ASCII_FORMATTED_CHARSET X-ASCII-Formatted} (alias: {@link #ASCII_FORMATTED_ALIAS
* ASCII-Formatted})
* <li>{@link #TRANSLITERATING_CHARSET X-Transliterating}
* <li>{@link #TRANSLITERATING_SINGLE_BYTE_CHARSET X-Transliterating-Single-Byte} (alias: {@link
* #ACH_ALIAS ACH})
* </ul>
*
* @param charsetName the name of the requested charset
* @return the corresponding Charset object, or null if the requested charset is not supported
* {@code @ThreadSafe} This method is thread-safe
*/
@Override
public Charset charsetForName(final String charsetName) {
if (charsetName == null) {
return null;
}
return switch (charsetName.toUpperCase(Locale.ROOT)) {
case "X-ASCII-PRINTABLE", "ASCII-PRINTABLE" -> CHARSETS.get(0);
case "X-ASCII-PLAIN", "ASCII-PLAIN" -> CHARSETS.get(1);
case "X-ASCII-FORMATTED", "ASCII-FORMATTED" -> CHARSETS.get(2);
case "X-TRANSLITERATING" -> CHARSETS.get(3);
case "X-TRANSLITERATING-SINGLE-BYTE", "ACH" -> CHARSETS.get(4);
default -> null;
};
}
}