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.Arrays;
import java.util.Iterator;
import java.util.List;
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 Charset asciiPrintable;

  private Charset asciiPlain;

  private Charset asciiFormatted;

  private Charset transliterating;

  private Charset transliteratingSingleByte;

  private List<Charset> charsets;

  /**
   * Creates a new TransliteratingASCIIProvider instance.
   *
   * <p>This provider will lazily initialize the available character sets upon the first request.
   */
  public TransliteratingASCIIProvider() {
    // Default constructor
  }

  private Charset getAsciiPrintable() {
    if (asciiPrintable == null) {
      final ASCIIFilter filter = new ASCIIFilter(Character.CONTROL);
      final Cache transliterator = new Cache(filter);
      asciiPrintable =
          new TransliteratingASCII(transliterator, ASCII_PRINTABLE_CHARSET, ASCII_PRINTABLE_ALIAS);
    }
    return asciiPrintable;
  }

  private Charset getAsciiPlain() {
    if (asciiPlain == null) {
      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
      asciiPlain = new TransliteratingASCII(cache, ASCII_PLAIN_CHARSET, ASCII_PLAIN_ALIAS);
    }
    return asciiPlain;
  }

  private Charset getAsciiFormatted() {
    if (asciiFormatted == null) {
      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
      asciiFormatted =
          new TransliteratingASCII(cache, ASCII_FORMATTED_CHARSET, ASCII_FORMATTED_ALIAS);
    }
    return asciiFormatted;
  }

  private Charset getTransliterating() {
    if (transliterating == null) {
      final ASCIIFilter filter = new ASCIIFilter();
      final IntFunction<CharSequence> transliterator = new Decompose(new Name(filter));
      final Cache cache = new Cache(transliterator);
      transliterating = new TransliteratingASCII(cache, TRANSLITERATING_CHARSET);
    }
    return transliterating;
  }

  private Charset getTransliteratingSingleByte() {
    if (transliteratingSingleByte == null) {
      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);
      transliteratingSingleByte =
          new TransliteratingASCII(cache, TRANSLITERATING_SINGLE_BYTE_CHARSET, ACH_ALIAS);
    }
    return transliteratingSingleByte;
  }

  /**
   * 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() {
    synchronized (this) {
      if (charsets == null) {
        charsets =
            Arrays.asList(
                getAsciiPrintable(),
                getAsciiPlain(),
                getAsciiFormatted(),
                getTransliterating(),
                getTransliteratingSingleByte());
      }
    }
    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(String charsetName) {
    return switch (charsetName) {
      case ASCII_PRINTABLE_CHARSET, ASCII_PRINTABLE_ALIAS -> getAsciiPrintable();
      case ASCII_PLAIN_CHARSET, ASCII_PLAIN_ALIAS -> getAsciiPlain();
      case ASCII_FORMATTED_CHARSET, ASCII_FORMATTED_ALIAS -> getAsciiFormatted();
      case TRANSLITERATING_CHARSET -> getTransliterating();
      case TRANSLITERATING_SINGLE_BYTE_CHARSET, ACH_ALIAS -> getTransliteratingSingleByte();
      default -> null;
    };
  }
}