IonTextUtils.java

/*
 * Copyright 2007-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.amazon.ion.util;

import static com.amazon.ion.impl._Private_IonConstants.isHighSurrogate;
import static com.amazon.ion.impl._Private_IonConstants.isLowSurrogate;
import static com.amazon.ion.impl._Private_IonConstants.makeUnicodeScalar;
import static com.amazon.ion.impl._Private_IonTextAppender.ZERO_PADDING;
import static com.amazon.ion.impl._Private_IonTextAppender.isIdentifierKeyword;
import static com.amazon.ion.impl._Private_IonTextAppender.symbolNeedsQuoting;
import static com.amazon.ion.impl._Private_IonTextWriterBuilder.STANDARD;

import com.amazon.ion.SymbolToken;
import com.amazon.ion.impl._Private_IonTextAppender;
import java.io.IOException;
import java.math.BigDecimal;


/**
 * Utility methods for working with Ion's text-oriented data types.
 */
public class IonTextUtils
{

    public enum SymbolVariant { IDENTIFIER, OPERATOR, QUOTED }

    private enum EscapeMode { JSON, ION_SYMBOL, ION_STRING, ION_LONG_STRING }


    /**
     * Ion whitespace is defined as one of the characters space, tab, newline,
     * and carriage-return.  This matches the definition of whitespace used by
     * JSON.
     *
     * @param codePoint the character to test.
     * @return <code>true</code> if <code>c</code> is one of the four legal
     * Ion whitespace characters.
     *
     * @see <a href="http://tools.ietf.org/html/rfc4627">RFC 4627</a>
     */
    public static boolean isWhitespace(int codePoint)
    {
        switch (codePoint)
        {
            // FIXME look for BOM
            case ' ':  case '\t':  case '\n':  case '\r':
            {
                return true;
            }
            default:
            {
                return false;
            }
        }
    }

    /**
     * Ion whitespace is defined as one of the characters space, tab, newline,
     * and carriage-return.  This matches the definition of whitespace used by
     * JSON.
     *
     * @param charSequence the CharSequence to test.
     * @return {@code true} if {@code charSequence} consists entirely of the four legal
     * Ion whitespace characters.
     *
     * @see #isWhitespace(int)
     * @see <a href="http://tools.ietf.org/html/rfc4627">RFC 4627</a>
     */
    public static boolean isAllWhitespace(CharSequence charSequence) {
        for (int i = 0; i < charSequence.length(); i++) {
            if (!IonTextUtils.isWhitespace(Character.codePointAt(charSequence, i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Determines whether a given code point is one of the valid Ion numeric
     * terminators.
     * <p>
     * The slash character {@code '/'} is not itself a valid terminator, but
     * if the next character is {@code '/'} or {@code '*'} then the number is
     * followed by a comment.  Since this method cannot perform the look-ahead
     * necessary to make that determination, it returns {@code false} for the
     * slash.
     *
     * @param codePoint the Unicode scalar to test.
     *
     * @return true when the scalar can legally follow an Ion number.
     * Returns false for the slash character {@code '/'}.
     */
    public static boolean isNumericStop(int codePoint)
    {
        switch (codePoint) {
        case -1:
        case '{':  case '}':
        case '[':  case ']':
        case '(':  case ')':
        case ',':
        case '\"': case '\'':
        case ' ':  case '\t':  case '\n':  case '\r':  // Whitespace
        // case '/': // we check start of comment in the caller where we
        //              can peek ahead for the following slash or asterisk
            return true;

        default:
            return false;
        }
    }


    public static boolean isDigit(int codePoint, int radix)
    {
        switch (codePoint) {
        case '0': case '1': case '2': case '3': case '4':
        case '5': case '6': case '7':
            return (radix == 8 || radix == 10 || radix == 16);
        case '8': case '9':
            return (radix == 10 || radix == 16);
        case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
        case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
            return (radix == 16);
        }
        return false;
    }

    public static boolean isIdentifierStart(int codePoint) {
        return _Private_IonTextAppender.isIdentifierStart(codePoint);
    }

    public static boolean isIdentifierPart(int codePoint) {
        return _Private_IonTextAppender.isIdentifierPart(codePoint);
    }

    public static boolean isOperatorPart(int codePoint) {
        return _Private_IonTextAppender.isOperatorPart(codePoint);
    }



    /**
     * Determines whether the text of a symbol represents an identifier, an
     * operator, or a symbol that always requires (single) quotes.
     *
     * @param symbol must be a non-empty string.
     *
     * @return the variant of the symbol.
     *
     * @throws NullPointerException
     *         if <code>symbol</code> is <code>null</code>.
     */
    public static SymbolVariant symbolVariant(CharSequence symbol)
    {
        int length = symbol.length(); // acts as null check

        // If the symbol's text matches an Ion keyword or it's an empty symbol, we must quote it.
        // Eg, the symbol 'false' and '' must be rendered quoted.
        if(length == 0 || isIdentifierKeyword(symbol))
        {
            return SymbolVariant.QUOTED;
        }

        char c = symbol.charAt(0);
        // Surrogates are neither identifierStart nor operatorPart, so the
        // first one we hit will fall through and return QUOTED.
        // TODO test that

        if (isIdentifierStart(c))
        {
            for (int ii = 0; ii < length; ii++) {
                c = symbol.charAt(ii);
                if ((c == '\'' || c < 32 || c > 126)
                    || !isIdentifierPart(c))
                {
                    return SymbolVariant.QUOTED;
                }
            }
            return SymbolVariant.IDENTIFIER;
        }

        if (isOperatorPart(c))
        {
            for (int ii = 0; ii < length; ii++) {
                c = symbol.charAt(ii);
                // We don't need to look for escapes since all
                // operator characters are ASCII.
                if (!isOperatorPart(c)) {
                    return SymbolVariant.QUOTED;
                }
            }
            return SymbolVariant.OPERATOR;
        }

        return SymbolVariant.QUOTED;
    }


    //=========================================================================


    /**
     * Prints a single Unicode code point for use in an ASCII-safe Ion string.
     *
     * @param out the stream to receive the data.
     * @param codePoint a Unicode code point.
     */
    public static void printStringCodePoint(Appendable out, int codePoint)
        throws IOException
    {
        printCodePoint(out, codePoint, EscapeMode.ION_STRING);
    }

    /**
     * Prints a single Unicode code point for use in an ASCII-safe Ion symbol.
     *
     * @param out the stream to receive the data.
     * @param codePoint a Unicode code point.
     */
    public static void printSymbolCodePoint(Appendable out, int codePoint)
        throws IOException
    {
        printCodePoint(out, codePoint, EscapeMode.ION_SYMBOL);
    }

    /**
     * Prints a single Unicode code point for use in an ASCII-safe JSON string.
     *
     * @param out the stream to receive the data.
     * @param codePoint a Unicode code point.
     */
    public static void printJsonCodePoint(Appendable out, int codePoint)
        throws IOException
    {
        // JSON only allows double-quote strings.
        printCodePoint(out, codePoint, EscapeMode.JSON);
    }


    /**
     * Prints a single code point, ASCII safe.
     */
    private static void printCodePoint(Appendable out, int c, EscapeMode mode)
        throws IOException
    {
        // JSON only allows uHHHH numeric escapes.
        switch (c) {
            case 0:
                out.append(mode == EscapeMode.JSON ? "\\u0000" : "\\0");
                return;
            case '\t':
                out.append("\\t");
                return;
            case '\n':
                if (mode == EscapeMode.ION_LONG_STRING) {
                    out.append('\n');
                }
                else {
                    out.append("\\n");
                }
                return;
            case '\r':
                out.append("\\r");
                return;
            case '\f':
                out.append("\\f");
                return;
            case '\u0008':
                out.append("\\b");
                return;
            case '\u0007':
                out.append(mode == EscapeMode.JSON ? "\\u0007" : "\\a");
                return;
            case '\u000B':
                out.append(mode == EscapeMode.JSON ? "\\u000b" : "\\v");
                return;
            case '\"':
                if (mode == EscapeMode.JSON || mode == EscapeMode.ION_STRING) {
                    out.append("\\\"");
                    return;
                }
                break; // Treat as normal code point for long string or symbol.
            case '\'':
                if (mode == EscapeMode.ION_SYMBOL ||
                    mode == EscapeMode.ION_LONG_STRING)
                {
                    out.append("\\\'");
                    return;
                }
                break;
            case '\\':
                out.append("\\\\");
                return;
            default:
                break;
        }

        if (c < 32) {
            if (mode == EscapeMode.JSON) {
                printCodePointAsFourHexDigits(out, c);
            }
            else {
                printCodePointAsTwoHexDigits(out, c);
            }
        }
        else if (c < 0x7F) {  // Printable ASCII
            out.append((char)c);
        }
        else if (c <= 0xFF) {
            if (mode == EscapeMode.JSON) {
                printCodePointAsFourHexDigits(out, c);
            }
            else {
                printCodePointAsTwoHexDigits(out, c);
            }
        }
        else if (c <= 0xFFFF) {
            printCodePointAsFourHexDigits(out, c);
        }
        else {
            if (mode == EscapeMode.JSON) {
                // JSON does not have a \Uxxxxyyyy escape, surrogates
                // must be used as per RFC-4627
                //
                // http://www.ietf.org/rfc/rfc4627.txt
                // [...]
                //   To escape an extended character that is not in the Basic Multilingual
                //   Plane, the character is represented as a twelve-character sequence,
                //   encoding the UTF-16 surrogate pair.  So, for example, a string
                //   containing only the G clef character (U+1D11E) may be represented as
                //   "\uD834\uDD1E".
                // [...]
                printCodePointAsSurrogatePairHexDigits(out, c);
            }
            else {
                printCodePointAsEightHexDigits(out, c);
            }
        }
    }


    /**
     * Generates a two-digit hex escape sequence,
     * {@code "\x}<i>{@code HH}</i>{@code "},
     * using lower-case for alphabetics.
     */
    private static void printCodePointAsTwoHexDigits(Appendable out, int c)
        throws IOException
    {
        String s = Integer.toHexString(c);
        out.append("\\x");
        if (s.length() < 2) {
            out.append(ZERO_PADDING[2-s.length()]);
        }
        out.append(s);
    }

    /**
     * Generates a four-digit hex escape sequence,
     * {@code "\}{@code u}<i>{@code HHHH}</i>{@code "},
     * using lower-case for alphabetics.
     */
    private static void printCodePointAsFourHexDigits(Appendable out, int c)
        throws IOException
    {
        String s = Integer.toHexString(c);
        out.append("\\u");
        out.append(ZERO_PADDING[4-s.length()]);
        out.append(s);
    }

    /**
     * Prints an eight-digit hex escape sequence,
     * {@code "\}{@code U}<i>{@code HHHHHHHH}</i>{@code "},
     * using lower-case for alphabetics.
     */
    private static void printCodePointAsEightHexDigits(Appendable out, int c)
        throws IOException
    {
        String s = Integer.toHexString(c);
        out.append("\\U");
        out.append(ZERO_PADDING[8-s.length()]);
        out.append(s);
    }

    /**
     * Generates a surrogate pair as two four-digit hex escape sequences,
     * {@code "\}{@code u}<i>{@code HHHH}</i>{@code \}{@code u}<i>{@code HHHH}</i>{@code "},
     * using lower-case for alphabetics.
     * This for necessary for JSON when the code point is outside the BMP.
     */
    private static void printCodePointAsSurrogatePairHexDigits(Appendable out, int c)
        throws IOException
    {
        for (final char unit : Character.toChars(c)) {
            printCodePointAsFourHexDigits(out, unit);
        }
    }


    /**
     * Prints characters as an ASCII-encoded Ion string, including surrounding
     * double-quotes.
     * If the {@code text} is null, this prints {@code null.string}.
     *
     * @param out the stream to receive the data.
     * @param text the text to print; may be {@code null}.
     *
     * @throws IOException if the {@link Appendable} throws an exception.
     * @throws IllegalArgumentException
     *     if the text contains invalid UTF-16 surrogates.
     */
    public static void printString(Appendable out, CharSequence text)
        throws IOException
    {
        if (text == null)
        {
            out.append("null.string");
        }
        else
        {
            out.append('"');
            printCodePoints(out, text, EscapeMode.ION_STRING);
            out.append('"');
        }
    }

    /**
     * Prints characters as an ASCII-encoded JSON string, including surrounding
     * double-quotes.
     * If the {@code text} is null, this prints {@code null}.
     *
     * @param out the stream to receive the JSON data.
     * @param text the text to print; may be {@code null}.
     *
     * @throws IOException if the {@link Appendable} throws an exception.
     * @throws IllegalArgumentException
     *     if the text contains invalid UTF-16 surrogates.
     */
    public static void printJsonString(Appendable out, CharSequence text)
        throws IOException
    {
        if (text == null)
        {
            out.append("null");
        }
        else
        {
            out.append('"');
            printCodePoints(out, text, EscapeMode.JSON);
            out.append('"');
        }
    }


    /**
     * Builds a String denoting an ASCII-encoded Ion string,
     * including surrounding double-quotes.
     * If the {@code text} is null, this returns {@code "null.string"}.
     *
     * @param text the text to print; may be {@code null}.
     *
     * @throws IllegalArgumentException
     *     if the text contains invalid UTF-16 surrogates.
     */
    public static String printString(CharSequence text)
    {
        if (text == null)
        {
            return "null.string";
        }
        if (text.length() == 0)
        {
            return "\"\"";
        }

        StringBuilder builder = new StringBuilder(text.length() + 2);
        try
        {
            printString(builder, text);
        }
        catch (IOException e)
        {
            // Shouldn't happen
            throw new Error(e);
        }
        return builder.toString();
    }


    /**
     * Builds a String denoting an ASCII-encoded Ion "long string",
     * including surrounding triple-quotes.
     * If the {@code text} is null, this returns {@code "null.string"}.
     *
     * @param text the text to print; may be {@code null}.
     *
     * @throws IllegalArgumentException
     *     if the text contains invalid UTF-16 surrogates.
     */
    public static String printLongString(CharSequence text)
    {
        if (text == null) return "null.string";

        if (text.length() == 0) return "''''''";

        StringBuilder builder = new StringBuilder(text.length() + 6);
        try
        {
            printLongString(builder, text);
        }
        catch (IOException e)
        {
            // Shouldn't happen
            throw new Error(e);
        }
        return builder.toString();
    }


    /**
     * Prints characters as an ASCII-encoded Ion "long string",
     * including surrounding triple-quotes.
     * If the {@code text} is null, this prints {@code null.string}.
     *
     * @param out the stream to receive the data.
     * @param text the text to print; may be {@code null}.
     *
     * @throws IOException if the {@link Appendable} throws an exception.
     * @throws IllegalArgumentException
     *     if the text contains invalid UTF-16 surrogates.
     */
    public static void printLongString(Appendable out, CharSequence text)
        throws IOException
    {
        if (text == null)
        {
            out.append("null.string");
        }
        else
        {
            out.append("'''");
            printCodePoints(out, text, EscapeMode.ION_LONG_STRING);
            out.append("'''");
        }
    }


    /**
     * Builds a String denoting an ASCII-encoded Ion string,
     * with double-quotes surrounding a single Unicode code point.
     *
     * @param codePoint a Unicode code point.
     */
    public static String printCodePointAsString(int codePoint)
    {
        StringBuilder builder = new StringBuilder(12);
        builder.append('"');
        try
        {
            printStringCodePoint(builder, codePoint);
        }
        catch (IOException e)
        {
            // Shouldn't happen
            throw new Error(e);
        }
        builder.append('"');
        return builder.toString();
    }


    /**
     * Prints the text as an Ion symbol, including surrounding single-quotes if
     * they are necessary.  Operator symbols such as {@code '+'} are quoted.
     * If the {@code text} is null, this prints {@code null.symbol}.
     *
     * @param out the stream to receive the Ion data.
     * @param text the symbol text; may be {@code null}.
     *
     * @throws IOException if the {@link Appendable} throws an exception.
     *
     * @see #printSymbol(CharSequence)
     */
    public static void printSymbol(Appendable out, CharSequence text)
        throws IOException
    {
        if (text == null)
        {
            out.append("null.symbol");
        }
        else if (symbolNeedsQuoting(text, true))
        {
            printQuotedSymbol(out, text);
        }
        else
        {
            out.append(text);
        }
    }


    /**
     * Prints the text as an Ion symbol, including surrounding single-quotes if
     * they are necessary.  Operator symbols such as {@code '+'} are quoted.
     * If the {@code text} is null, this returns {@code "null.symbol"}.
     *
     * @param text the symbol text; may be {@code null}.
     *
     * @return a string containing the resulting Ion data.
     *
     * @see #printSymbol(Appendable, CharSequence)
     */
    public static String printSymbol(CharSequence text)
    {
        if (text == null)
        {
            return "null.symbol";
        }

        StringBuilder builder = new StringBuilder(text.length() + 2);
        try
        {
            printSymbol(builder, text);
        }
        catch (IOException e)
        {
            // Shouldn't happen
            throw new Error(e);
        }
        return builder.toString();
    }


    /**
     * @param token the symbolToken to be printed.
     *
     * @return a string representing the symboltoken.
     */
    public static String printSymbol(SymbolToken token){
        return String.format("{$%s:%s}",  token.getText(), token.getSid());
    }


    /**
     * Prints text as a single-quoted Ion symbol.
     * If the {@code text} is null, this prints {@code null.symbol}.
     *
     * @param out the stream to receive the data.
     * @param text the symbol text; may be {@code null}.
     *
     * @throws IOException if the {@link Appendable} throws an exception.
     * @throws IllegalArgumentException
     *     if the text contains invalid UTF-16 surrogates.
     */
    public static void printQuotedSymbol(Appendable out, CharSequence text)
        throws IOException
    {
        if (text == null)
        {
            out.append("null.symbol");
        }
        else
        {
            out.append('\'');
            printCodePoints(out, text, EscapeMode.ION_SYMBOL);
            out.append('\'');
        }
    }


    /**
     * Builds a String containing a single-quoted Ion symbol.
     * If the {@code text} is null, this returns {@code "null.symbol"}.
     *
     * @param text the symbol text; may be {@code null}.
     *
     * @throws IllegalArgumentException
     *     if the text contains invalid UTF-16 surrogates.
     */
    public static String printQuotedSymbol(CharSequence text)
    {
        if (text == null)
        {
            return "null.symbol";
        }

        StringBuilder builder = new StringBuilder(text.length() + 2);
        try
        {
            printQuotedSymbol(builder, text);
        }
        catch (IOException e)
        {
            // Shouldn't happen
            throw new Error(e);
        }
        return builder.toString();
    }


    /**
     * @throws IllegalArgumentException
     *     if the text contains invalid UTF-16 surrogates.
     */
    private static void printCodePoints(Appendable out, CharSequence text,
                                        EscapeMode mode)
        throws IOException
    {
        int len = text.length();
        for (int i = 0; i < len; i++)
        {
            int c = text.charAt(i);

            if (isHighSurrogate(c))
            {
                i++;
                char c2;
                // I apologize for the embedded assignment, but the alternative
                // was worse.
                if (i >= len || !isLowSurrogate(c2 = text.charAt(i)))
                {
                    String message =
                        "text is invalid UTF-16. It contains an unmatched " +
                        "high surrogate 0x" + Integer.toHexString(c) +
                        " at index " + i;
                    throw new IllegalArgumentException(message);
                }
                c = makeUnicodeScalar(c, c2);
            }
            else if (isLowSurrogate(c))
            {
                String message =
                    "text is invalid UTF-16. It contains an unmatched " +
                    "low surrogate 0x" + Integer.toHexString(c) +
                    " at index " + i;
                throw new IllegalArgumentException(message);
            }

            printCodePoint(out, c, mode);
        }
    }


    ////////////////////////////////////

    // from the Java 1.5 spec at (with extra spacing to keep the parser happy):
    // http://java.sun.com/docs/books/jls/second_edition/html/lexical.doc.html#101089
    //
    //    EscapeSequence:
    //        \ b            = \ u 0008: backspace BS
    //        \ t            = \ u 0009: horizontal tab HT
    //        \ n            = \ u 000a: linefeed LF
    //        \ f            = \ u 000c: form feed FF
    //        \ r            = \ u 000d: carriage return CR
    //        \ "            = \ u 0022: double quote "
    //        \ '            = \ u 0027: single quote '
    //        \ \            = \ u 005c: backslash \
    //        \ 0            = \ u 0000: NUL

    // from the JSON RFC4627 at:
    // http://www.faqs.org/rfcs/rfc4627.html
    //    escape (
    //        %x22 /          ; "    quotation mark  U+0022
    //        %x5C /          ; \    reverse solidus U+005C
    //        %x2F /          ; /    solidus         U+002F
    //        %x62 /          ; b    backspace       U+0008
    //        %x66 /          ; f    form feed       U+000C
    //        %x6E /          ; n    line feed       U+000A
    //        %x72 /          ; r    carriage return U+000D
    //        %x74 /          ; t    tab             U+0009
    //        %x75 4HEXDIG )  ; uXXXX                U+XXXX
    //
    // escape = %x5C              ;

    // c++ character escapes from:
    //    http://www.cplusplus.com/doc/tutorial/constants.html
    //    \n newline
    //    \r carriage return
    //    \t tab
    //    \v vertical tab
    //    \b backspace
    //    \f form feed (page feed)
    //    \a alert (beep)
    //    \' single quote (')
    //    \" double quote (")
    //    \? question mark (?)
    //    \\ backslash (\)

    // from http://msdn2.microsoft.com/en-us/library/ms860944.aspx
    //    Table 1.4   Escape Sequences
    //    Escape Sequence Represents
    //    \a Bell (alert)
    //    \b Backspace
    //    \f Formfeed
    //    \n New line
    //    \r Carriage return
    //    \t Horizontal tab
    //    \v Vertical tab
    //    \' Single quotation mark
    //    \"  Double quotation mark
    //    \\ Backslash
    //    \? Literal question mark
    //    \ooo ASCII character in octal notation
    //    \xhhh ASCII character in hexadecimal notation

    //    Ion escapes, actual set:
    //     \a Bell (alert)
    //     \b Backspace
    //     \f Formfeed
    //     \n New line
    //     \r Carriage return
    //     \t Horizontal tab
    //     \v Vertical tab
    //     \' Single quotation mark
    //     xx not included: \` Accent grave
    //     \"  Double quotation mark
    //     \\ Backslash (solidus)
    //     \/ Slash (reverse solidus) U+005C
    //     \? Literal question mark
    //     \0 nul character
    //
    //     \xhh ASCII character in hexadecimal notation (1-2 digits)
    //    \\uhhhh Unicode character in hexadecimal
    //     \Uhhhhhhhh Unicode character in hexadecimal


    //=========================================================================
    // Numeric scalars


    public static void printDecimal(Appendable out, BigDecimal decimal)
        throws IOException
    {
        _Private_IonTextAppender appender =
            _Private_IonTextAppender.forAppendable(out);
        appender.printDecimal(STANDARD, decimal);
    }

    public static String printDecimal(BigDecimal decimal)
    {
        if (decimal == null)
        {
            return "null.decimal";
        }

        StringBuilder builder = new StringBuilder(64);
        try
        {
            printDecimal(builder, decimal);
        }
        catch (IOException e)
        {
            // Shouldn't happen
            throw new Error(e);
        }
        return builder.toString();
    }


    public static void printFloat(Appendable out, double value)
        throws IOException
    {
        _Private_IonTextAppender appender =
            _Private_IonTextAppender.forAppendable(out);
        appender.printFloat(STANDARD, value);
    }

    public static String printFloat(double value)
    {
        StringBuilder builder = new StringBuilder(64);
        try
        {
            printFloat(builder, value);
        }
        catch (IOException e)
        {
            // Shouldn't happen
            throw new Error(e);
        }
        return builder.toString();
    }


    public static void printFloat(Appendable out, Double value)
        throws IOException
    {
        _Private_IonTextAppender appender =
            _Private_IonTextAppender.forAppendable(out);
        appender.printFloat(STANDARD, value);
    }

    public static String printFloat(Double value)
    {
        if (value == null)
        {
            return "null.float";
        }

        return printFloat(value.doubleValue());
    }


    //=========================================================================
    // LOBs


    public static void printBlob(Appendable out, byte[] value)
        throws IOException
    {
        if (value == null)
        {
            out.append("null.blob");
        }
        else
        {
            _Private_IonTextAppender appender =
                _Private_IonTextAppender.forAppendable(out);
            appender.printBlob(STANDARD, value, 0, value.length);
        }
    }

    public static String printBlob(byte[] value)
    {
        if (value == null)
        {
            return "null.blob";
        }

        StringBuilder builder = new StringBuilder(64);
        try
        {
            printBlob(builder, value);
        }
        catch (IOException e)
        {
            // Shouldn't happen
            throw new Error(e);
        }
        return builder.toString();
    }


    public static void printClob(Appendable out, byte[] value)
        throws IOException
    {
        if (value == null)
        {
            out.append("null.clob");
        }
        else
        {
            _Private_IonTextAppender appender =
                _Private_IonTextAppender.forAppendable(out);
            appender.printClob(STANDARD, value, 0, value.length);
        }
    }

    public static String printClob(byte[] value)
    {
        if (value == null)
        {
            return "null.clob";
        }

        StringBuilder builder = new StringBuilder(64);
        try
        {
            printClob(builder, value);
        }
        catch (IOException e)
        {
            // Shouldn't happen
            throw new Error(e);
        }
        return builder.toString();
    }
}