Printer.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.SystemSymbols.IMPORTS;
import static com.amazon.ion.SystemSymbols.ION_1_0_SID;
import static com.amazon.ion.SystemSymbols.ION_SYMBOL_TABLE;
import static com.amazon.ion.SystemSymbols.SYMBOLS;

import com.amazon.ion.Decimal;
import com.amazon.ion.IonBlob;
import com.amazon.ion.IonBool;
import com.amazon.ion.IonClob;
import com.amazon.ion.IonDatagram;
import com.amazon.ion.IonDecimal;
import com.amazon.ion.IonFloat;
import com.amazon.ion.IonInt;
import com.amazon.ion.IonList;
import com.amazon.ion.IonNull;
import com.amazon.ion.IonReader;
import com.amazon.ion.IonSequence;
import com.amazon.ion.IonSexp;
import com.amazon.ion.IonString;
import com.amazon.ion.IonStruct;
import com.amazon.ion.IonSymbol;
import com.amazon.ion.IonTimestamp;
import com.amazon.ion.IonType;
import com.amazon.ion.IonValue;
import com.amazon.ion.IonWriter;
import com.amazon.ion.SymbolTable;
import com.amazon.ion.SymbolToken;
import com.amazon.ion.Timestamp;
import com.amazon.ion.impl._Private_IonSymbol;
import com.amazon.ion.impl._Private_IonSystem;
import com.amazon.ion.impl._Private_IonTextWriterBuilder;
import com.amazon.ion.impl._Private_IonValue;
import com.amazon.ion.impl._Private_IonValue.SymbolTableProvider;
import com.amazon.ion.system.IonTextWriterBuilder;
import com.amazon.ion.system.IonTextWriterBuilder.LstMinimizing;
import com.amazon.ion.system.IonWriterBuilder.IvmMinimizing;
import com.amazon.ion.util.IonTextUtils.SymbolVariant;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.util.Iterator;


/**
 * <b>DEPRECATED: use {@link IonValue#writeTo(IonWriter)} instead.</b>
 * <p>
 * Renders {@link IonValue}s to text.
 * <p>
 * By default, output is in a compact format with minimal whitespace.
 * For example:
 *<pre>
 *    annot::{f1:["hello","goodbye"],'another field':long::0}
 *</pre>
 * The format can be tuned through various properties on the Printer instance,
 * as well as through the {@link Printer.Options} structure.
 * <p>
 * <b>Instances of this class are safe for use by multiple threads.</b>
 * <p>
 * While printers are inexpensive to create, their configuration facilities
 * make them useful as shared resources. Changes to configuration settings
 * (<em>e.g.</em>, {@link #setJsonMode()}) do not affect concurrently-running
 * calls to {@link #print}.
 *
 * @see IonWriter
 * @see IonTextWriterBuilder
 * @deprecated Use {@link IonValue#writeTo(IonWriter)} instead.
 */
@Deprecated
public class Printer
{
    public class Options
        implements Cloneable
    {
        public boolean blobAsString;
        public boolean clobAsString;
        public boolean datagramAsList;
        public boolean decimalAsFloat;
        public boolean sexpAsList;
        public boolean skipAnnotations;
        public boolean skipSystemValues;
        public boolean simplifySystemValues;
        public boolean stringAsJson;
        public boolean symbolAsString;
        public boolean timestampAsString;
        public boolean timestampAsMillis;
        public boolean untypedNulls;


        @Override
        public Options clone()
        {
            try
            {
                return (Options) super.clone();
            }
            catch (CloneNotSupportedException e)
            {
                throw new InternalError();
            }
        }
    }


    protected Options myOptions = new Options();

    public Printer()
    {
        myOptions = new Options();
    }

    public Printer(Options options)
    {
        myOptions = options.clone();
    }


    //=========================================================================
    // Options

    /* Potential printing options:
     *
     * - Render all times with a specific offset (what about unknowns?)
     * - Render all times with long offset (no Z)
     * - control over decimal-point placement in float and decimal.
     *   (render 12.00 or 1200d-2)
     */


    /**
     * Indicates whether this printer skips (<em>i.e.</em>, doesn't print)
     * system IDs and local symbol tables.
     * By default, this property is <code>false</code>.
     */
    public synchronized boolean getSkipSystemValues()
    {
        return myOptions.skipSystemValues;
    }

    /**
     * Sets whether this printer skips (<em>i.e.</em>, doesn't print)
     * system IDs and local symbol tables.
     * By default, this property is <code>false</code>.
     */
    public synchronized void setSkipSystemValues(boolean skip)
    {
        myOptions.skipSystemValues = skip;
    }


    /**
     * Indicates whether this printer skips (<em>i.e.</em>, doesn't print)
     * annotations.
     * By default, this property is <code>false</code>.
     */
    public synchronized boolean getSkipAnnotations()
    {
        return myOptions.skipAnnotations;
    }

    /**
     * Sets whether this printer skips (<em>i.e.</em>, doesn't print)
     * annotations.
     * By default, this property is <code>false</code>.
     */
    public synchronized void setSkipAnnotations(boolean skip)
    {
        myOptions.skipAnnotations = skip;
    }


    /**
     * Indicates whether this printer renders blobs as Base64 strings.
     * By default, this is <code>false</code>.
     */
    public synchronized boolean getPrintBlobAsString()
    {
        return myOptions.blobAsString;
    }

    /**
     * Sets whether this printer renders blobs as Base64 strings.
     * By default, this is <code>false</code>.
     */
    public synchronized void setPrintBlobAsString(boolean blobAsString)
    {
        myOptions.blobAsString = blobAsString;
    }


    /**
     * Indicates whether this printer renders clobs as ASCII strings.
     * By default, this is <code>false</code>.
     */
    public synchronized boolean getPrintClobAsString()
    {
        return myOptions.clobAsString;
    }

    /**
     * Sets whether this printer renders clobs as ASCII strings.
     * By default, this is <code>false</code>.
     */
    public synchronized void setPrintClobAsString(boolean clobAsString)
    {
        myOptions.clobAsString = clobAsString;
    }


    /**
     * Indicates whether this printer renders datagrams as lists.
     * By default, this property is <code>false</code>.
     */
    public synchronized boolean getPrintDatagramAsList()
    {
        return myOptions.datagramAsList;
    }

    /**
     * Sets whether this printer renders datagrams as lists.
     * By default, this property is <code>false</code>.
     */
    public synchronized void setPrintDatagramAsList(boolean datagramAsList)
    {
        myOptions.datagramAsList = datagramAsList;
    }


    /**
     * Indicates whether this printer renders decimals as floats, thus using 'e'
     * notation for all real values.
     * By default, this is <code>false</code>.
     */
    public synchronized boolean getPrintDecimalAsFloat()
    {
        return myOptions.decimalAsFloat;
    }

    /**
     * Sets whether this printer renders decimals as floats, thus using 'e'
     * notation for all real values.
     * By default, this is <code>false</code>.
     */
    public synchronized void setPrintDecimalAsFloat(boolean decimalAsFloat)
    {
        myOptions.decimalAsFloat = decimalAsFloat;
    }


    /**
     * Indicates whether this printer renders sexps as lists.
     * By default, this is <code>false</code>.
     */
    public synchronized boolean getPrintSexpAsList()
    {
        return myOptions.sexpAsList;
    }

    /**
     * Sets whether this printer renders sexps as lists.
     * By default, this is <code>false</code>.
     */
    public synchronized void setPrintSexpAsList(boolean sexpAsList)
    {
        myOptions.sexpAsList = sexpAsList;
    }


    /**
     * Indicates whether this printer renders strings using JSON escapes.
     * By default, this is <code>false</code>.
     */
    public synchronized boolean getPrintStringAsJson()
    {
        return myOptions.stringAsJson;
    }

    /**
     * Sets whether this printer renders strings using JSON escapes.
     * By default, this is <code>false</code>.
     */
    public synchronized void setPrintStringAsJson(boolean stringAsJson)
    {
        myOptions.stringAsJson = stringAsJson;
    }


    /**
     * Indicates whether this printer renders symbols as strings.
     * By default, this is <code>false</code>.
     */
    public synchronized boolean getPrintSymbolAsString()
    {
        return myOptions.symbolAsString;
    }

    /**
     * Sets whether this printer renders symbols as strings.
     * By default, this is <code>false</code>.
     */
    public synchronized void setPrintSymbolAsString(boolean symbolAsString)
    {
        myOptions.symbolAsString = symbolAsString;
    }


    /**
     * Indicates whether this printer renders timestamps as millisecond values.
     * By default, this is <code>false</code>.
     */
    public synchronized boolean getPrintTimestampAsMillis()
    {
        return myOptions.timestampAsMillis;
    }

    /**
     * Sets whether this printer renders timestamps as millisecond values.
     * By default, this is <code>false</code>.
     */
    public synchronized void setPrintTimestampAsMillis(boolean timestampAsMillis)
    {
        myOptions.timestampAsMillis = timestampAsMillis;
    }


    /**
     * Indicates whether this printer renders timestamps as strings.
     * By default, this is <code>false</code>.
     */
    public synchronized boolean getPrintTimestampAsString()
    {
        return myOptions.timestampAsString;
    }

    /**
     * Sets whether this printer renders timestamps as strings.
     * By default, this is <code>false</code>.
     */
    public synchronized void setPrintTimestampAsString(boolean timestampAsString)
    {
        myOptions.timestampAsString = timestampAsString;
    }


    /**
     * Indicates whether this printer renders all null values as {@code null}
     * (<em>i.e.</em>, the same as an {@link IonNull}).
     * By default, this is <code>false</code>.
     */
    public synchronized boolean getPrintUntypedNulls()
    {
        return myOptions.untypedNulls;
    }

    /**
     * Sets whether this printer renders all null values as {@code null}
     * (<em>i.e.</em>, the same as an {@link IonNull}).
     * By default, this is <code>false</code>.
     */
    public synchronized void setPrintUntypedNulls(boolean untypedNulls)
    {
        myOptions.untypedNulls = untypedNulls;
    }


    /**
     * Configures this printer's options to render legal JSON text.
     * The following options are modified so that:
     * <ul>
     *   <li>{@link Options#blobAsString} is {@code true}</li>
     *   <li>{@link Options#clobAsString} is {@code true}</li>
     *   <li>{@link Options#datagramAsList} is {@code true}</li>
     *   <li>{@link Options#decimalAsFloat} is {@code true}</li>
     *   <li>{@link Options#sexpAsList} is {@code true}</li>
     *   <li>{@link Options#skipAnnotations} is {@code true}</li>
     *   <li>{@link Options#skipSystemValues} is {@code true}</li>
     *   <li>{@link Options#stringAsJson} is {@code true}</li>
     *   <li>{@link Options#symbolAsString} is {@code true}</li>
     *   <li>{@link Options#timestampAsString} is {@code false}</li>
     *   <li>{@link Options#timestampAsMillis} is {@code true}</li>
     *   <li>{@link Options#untypedNulls} is {@code true}</li>
     * </ul>
     * All other options are left as is.
     */
    public synchronized void setJsonMode()
    {
        myOptions.blobAsString      = true;
        myOptions.clobAsString      = true;
        myOptions.datagramAsList    = true;
        myOptions.decimalAsFloat    = true;
        myOptions.sexpAsList        = true;
        myOptions.skipAnnotations   = true;
        myOptions.skipSystemValues  = true;
        myOptions.stringAsJson      = true;
        myOptions.symbolAsString    = true;
        myOptions.timestampAsString = false;
        myOptions.timestampAsMillis = true;
        myOptions.untypedNulls      = true;
    }


    //=========================================================================
    // Print methods


    public void print(IonValue value, Appendable out)
        throws IOException
    {
        // Copy the options so visitor won't see changes made while printing.
        Options options;
        synchronized (this)  // So we don't clone in the midst of changes
        {
            options = myOptions.clone();
        }

        if (true)
        {
            _print(value, makeVisitor(options, out));
        }
        else
        {
            // Bridge to the configurable text writer. This is here for
            // testing purposes. It *almost* works except for printing
            // datagram as list.

            boolean dg = value instanceof IonDatagram;

            _Private_IonTextWriterBuilder o =
                _Private_IonTextWriterBuilder.standard();
            o.setCharset(IonTextWriterBuilder.ASCII);
            if (dg)
            {
                if (options.skipSystemValues)
                {
                    o.withMinimalSystemData();
                }
                else if (options.simplifySystemValues) {

                    o.setIvmMinimizing(IvmMinimizing.DISTANT);
                    o.setLstMinimizing(LstMinimizing.LOCALS);
                }
            }

            o._blob_as_string      = options.blobAsString;
            o._clob_as_string      = options.clobAsString;
            o._decimal_as_float    = options.decimalAsFloat;
            // TODO datagram as list
            o._sexp_as_list        = options.sexpAsList;
            o._skip_annotations    = options.skipAnnotations;
            o._string_as_json      = options.stringAsJson;
            o._symbol_as_string    = options.symbolAsString;
            o._timestamp_as_millis = options.timestampAsMillis;
            o._timestamp_as_string = options.timestampAsString;
            o._untyped_nulls       = options.untypedNulls;

            IonWriter writer = o.build(out);
            // TODO doesn't work for datagram since it skips system values
            // value.writeTo(writer);
            _Private_IonSystem system = (_Private_IonSystem) value.getSystem();
            IonReader reader = system.newSystemReader(value);
            writer.writeValues(reader);
            writer.finish();
        }
    }

    private void _print(IonValue value, PrinterVisitor pv)
        throws IOException
    {
        try
        {
            if (! (value instanceof IonDatagram))
            {
                pv.setSymbolTableProvider(new BasicSymbolTableProvider(value.getSymbolTable()));
            }
            value.accept(pv);
        }
        catch (IOException e)
        {
            throw e;
        }
        catch (RuntimeException e)
        {
            throw e;
        }
        catch (Exception e)
        {
            // Shouldn't happen.
            throw new RuntimeException(e);
        }
    }


    /**
     * Subclasses can override this if they wish to construct a specialization
     * of the {@link PrinterVisitor}.
     *
     * @param options is a fresh copy of the Printer's options instance,
     * not <code>null</code>.
     * @param out is not <code>null</code>.
     * @return the visitor to invoke for printing.
     */
    protected PrinterVisitor makeVisitor(Options options, Appendable out)
    {
        return new PrinterVisitor(options, out);
    }


    private static class BasicSymbolTableProvider
        implements SymbolTableProvider
    {
        private final SymbolTable symbolTable;

        public BasicSymbolTableProvider(SymbolTable symbolTable)
        {
            this.symbolTable = symbolTable;
        }

        public SymbolTable getSymbolTable()
        {
            return symbolTable;
        }

    }

    //=========================================================================
    // Print methods

    /**
     * @deprecated Use {@link IonValue#writeTo(IonWriter)} instead.
     */
    public static class PrinterVisitor
        extends AbstractValueVisitor
    {

        final protected Options    myOptions;
        final protected Appendable myOut;

        /**
         * Should we quote operators at the current level of the hierarchy?
         * As we recurse down into containers, this value is pushed on the
         * stack by {@link #writeChild(IonValue, boolean)}.
         */
        private boolean    myQuoteOperators = true;

        private SymbolTableProvider mySymbolTableProvider = null;


        //---------------------------------------------------------------------

        public PrinterVisitor(Options options, Appendable out)
        {
            myOptions = options;
            myOut = out;
        }

        void setSymbolTableProvider(SymbolTableProvider symbolTableProvider)
        {
            mySymbolTableProvider = symbolTableProvider;
        }

        /**
         * Recurse down into a container, we push the current value of
         * {@link #myQuoteOperators} onto the stack and replace it with
         * the given value.
         *
         * @param value
         * @param quoteOperators replaces the current value of
         * {@link #myQuoteOperators} during the recursive visitation.
         * @throws Exception propagated from visitation of <code>value</code>.
         * @throws NullPointerException if <code>value</code> is null.
         */
        protected void writeChild(IonValue value, boolean quoteOperators)
            throws Exception
        {
            boolean oldQuoteOperators = myQuoteOperators;
            myQuoteOperators = quoteOperators;
            value.accept(this);
            myQuoteOperators = oldQuoteOperators;
        }


        public void writeAnnotations(IonValue value) throws IOException
        {
            if (! myOptions.skipAnnotations)
            {
                SymbolToken[] anns = ((_Private_IonValue)value).getTypeAnnotationSymbols(mySymbolTableProvider);
                if (anns != null)
                {
                    for (SymbolToken ann : anns) {
                        String text = ann.getText();
                        if (text == null) {
                            myOut.append('$');
                            myOut.append(Integer.toString(ann.getSid()));
                        }
                        else {
                            IonTextUtils.printSymbol(myOut, text);
                        }
                        myOut.append("::");
                    }
                }
            }
        }

        public void writeNull(String type) throws IOException
        {
            if (myOptions.untypedNulls)
            {
                myOut.append("null");
            }
            else
            {
                myOut.append("null.");
                myOut.append(type);
            }
        }


        public void writeSequenceContent(IonSequence value,
                                         boolean quoteOperators,
                                         char open,
                                         char separator,
                                         char close)
            throws IOException, Exception
        {
            myOut.append(open);

            boolean hitOne = false;
            for (IonValue child : value)
            {
                if (hitOne)
                {
                    myOut.append(separator);
                }
                hitOne = true;

                writeChild(child, quoteOperators);
            }

            myOut.append(close);
        }

        public void writeSymbolToken(SymbolToken sym) throws IOException
        {
            String text = sym.getText();
            if (text != null)
            {
                writeSymbol(text);
            }
            else
            {
                int sid = sym.getSid();
                if (sid < 0)
                {
                    throw new IllegalArgumentException("Bad SID " + sid);
                }

                text = "$" + sym.getSid();
                if (myOptions.symbolAsString)
                {
                    writeString(text);
                }
                else
                {
                    myOut.append(text);  // SID literal is never quoted
                }
            }
        }

        public void writeSymbol(String text) throws IOException
        {
            if (myOptions.symbolAsString)
            {
                writeString(text);
            }
            else
            {
                SymbolVariant variant = IonTextUtils.symbolVariant(text);
                switch (variant)
                {
                    case IDENTIFIER:
                        myOut.append(text);
                        break;
                    case OPERATOR:
                        if (! myQuoteOperators)
                        {
                            myOut.append(text);
                            break;
                        }
                        // else fall through...
                    case QUOTED:
                        IonTextUtils.printQuotedSymbol(myOut, text);
                        break;
                }
            }
        }


        /**
         * @param text may be null
         */
        public void writeString(String text) throws IOException
        {
            if (myOptions.stringAsJson)
            {
                IonTextUtils.printJsonString(myOut, text);
            }
            else
            {
                IonTextUtils.printString(myOut, text);
            }
        }


        //---------------------------------------------------------------------
        // AbstractValueVisitor overrides

        @Override
        protected void defaultVisit(IonValue value)
        {
            String message = "cannot print " + value.getClass().getName();
            throw new UnsupportedOperationException(message);
        }

        @Override
        public void visit(IonBlob value) throws IOException
        {
            writeAnnotations(value);

            if (value.isNullValue())
            {
                writeNull("blob");
            }
            else
            {
                myOut.append(myOptions.blobAsString ? "\"" : "{{");
                value.printBase64(myOut);
                myOut.append(myOptions.blobAsString ? "\"" : "}}");
            }
        }

        @Override
        public void visit(IonBool value)
            throws IOException
        {
            writeAnnotations(value);

            if (value.isNullValue())
            {
                writeNull("bool");
            }
            else
            {
                myOut.append(value.booleanValue() ? "true" : "false");
            }
        }

        @Override
        public void visit(IonClob value) throws IOException
        {
            writeAnnotations(value);

            if (value.isNullValue())
            {
                writeNull("clob");
            }
            else
            {
                if (! myOptions.clobAsString)
                {
                    myOut.append("{{");
                }
                myOut.append('"');

                InputStream byteStream = value.newInputStream();
                try
                {
                    int c;
                    // if-statement hoisted above loop for efficiency
                    if (myOptions.stringAsJson)
                    {
                        while ((c = byteStream.read()) != -1)
                        {
                            IonTextUtils.printJsonCodePoint(myOut, c);
                        }
                    }
                    else
                    {
                        while ((c = byteStream.read()) != -1)
                        {
                            IonTextUtils.printStringCodePoint(myOut, c);
                        }
                    }
                }
                finally
                {
                    byteStream.close();
                }

                myOut.append('"');
                if (! myOptions.clobAsString)
                {
                    myOut.append("}}");
                }
            }
        }

        @Override
        public void visit(IonDatagram value) throws IOException, Exception
        {
            Iterator<IonValue> i = (myOptions.skipSystemValues
                                       ? value.iterator()
                                       : value.systemIterator());

            final boolean asList = myOptions.datagramAsList;
            if (asList)
            {
                myOut.append('[');
            }

            boolean hitOne = false;

            // If we're skipping system values at the iterator level,
            // we don't need to bother trying to simplify them.
            final boolean simplify_system_values =
                myOptions.simplifySystemValues && ! myOptions.skipSystemValues;

            SymbolTable previous_symbols = null;

            while (i.hasNext())
            {
                IonValue child = i.next();
                SymbolTable childSymbolTable = child.getSymbolTable();
                mySymbolTableProvider = new BasicSymbolTableProvider(childSymbolTable); //children of datagrams are top-level values
                if (simplify_system_values)
                {
                    child = simplify(child, previous_symbols);
                    previous_symbols = childSymbolTable;
                }

                if (child != null)
                {
                    if (hitOne)
                    {
                        myOut.append(asList  ?  ','  :  ' ');
                    }
                    writeChild(child, true);
                    hitOne = true; // we've only "hit one" if we wrote it
                }
            }

            if (asList)
            {
                myOut.append(']');
            }
        }

        private final IonValue simplify(IonValue child,
                                        SymbolTable previous_symbols)
        {
            IonType t = child.getType();
            switch (t) {
            case STRUCT:
                if (child.hasTypeAnnotation(ION_SYMBOL_TABLE)) {
                    if (symbol_table_struct_has_imports(child))
                    {
                        return ((IonStruct)child).cloneAndRemove(SYMBOLS);
                    }
                    return null;
                }
                // fall through to default (print the value)
                break;
            case SYMBOL:
                if (((IonSymbol)child).getSymbolId() == ION_1_0_SID) {
                    if (previous_symbols != null && previous_symbols.isSystemTable()) {
                        return null;
                    }
                    // fall through to default (print the value)
                }
                // fall through to default (print the value)
                break;
            default:
                break;
            }
            return child;
        }

        static final private boolean symbol_table_struct_has_imports(IonValue child) {
            IonStruct struct = (IonStruct)child;
            IonValue imports = struct.get(IMPORTS);
            if (imports instanceof IonList) {
                return ((IonList)imports).size() != 0;
            }
            return false;
        }

        @Override
        public void visit(IonDecimal value) throws IOException
        {
            writeAnnotations(value);

            if (value.isNullValue())
            {
                writeNull("decimal");
            }
            else
            {
                Decimal decimal = value.decimalValue();
                BigInteger unscaled = decimal.unscaledValue();

                int signum = decimal.signum();
                if (signum < 0)
                {
                    myOut.append('-');
                    unscaled = unscaled.negate();
                }
                else if (signum == 0 && decimal.isNegativeZero())
                {
                    // for the various forms of negative zero we have to
                    // write the sign ourselves, since neither BigInteger
                    // nor BigDecimal recognize negative zero, but Ion does.
                    myOut.append('-');
                }


                final String unscaledText = unscaled.toString();
                final int significantDigits = unscaledText.length();

                final int scale = decimal.scale();
                final int exponent = -scale;

                if (myOptions.decimalAsFloat)
                {
                    myOut.append(unscaledText);
                    myOut.append('e');
                    myOut.append(Integer.toString(exponent));
                }
                else if (exponent == 0)
                {
                    myOut.append(unscaledText);
                    myOut.append('.');
                }
                else if (0 < scale)
                {
                    int wholeDigits;
                    int remainingScale;
                    if (significantDigits > scale)
                    {
                        wholeDigits = significantDigits - scale;
                        remainingScale = 0;
                    }
                    else
                    {
                        wholeDigits = 1;
                        remainingScale = scale - significantDigits + 1;
                    }

                    myOut.append(unscaledText, 0, wholeDigits);
                    if (wholeDigits < significantDigits)
                    {
                        myOut.append('.');
                        myOut.append(unscaledText, wholeDigits,
                                     significantDigits);
                    }

                    if (remainingScale != 0)
                    {
                        myOut.append("d-");
                        myOut.append(Integer.toString(remainingScale));
                    }
                }
                else // (exponent > 0)
                {
                    // We cannot move the decimal point to the right, adding
                    // rightmost zeros, because that would alter the precision.
                    myOut.append(unscaledText);
                    myOut.append('d');
                    myOut.append(Integer.toString(exponent));
                }
            }
        }

        @Override
        public void visit(IonFloat value) throws IOException
        {
            writeAnnotations(value);

            if (value.isNullValue())
            {
                writeNull("float");
            }
            else
            {
                double real = value.doubleValue();
                IonTextUtils.printFloat(myOut, real);
            }
        }

        @Override
        public void visit(IonInt value) throws IOException
        {
            writeAnnotations(value);

            if (value.isNullValue())
            {
                writeNull("int");
            }
            else
            {
                myOut.append(value.bigIntegerValue().toString(10));
            }
        }

        @Override
        public void visit(IonList value) throws IOException, Exception
        {
            writeAnnotations(value);

            if (value.isNullValue())
            {
                writeNull("list");
            }
            else
            {
                writeSequenceContent(value, true, '[', ',', ']');
            }
        }

        @Override
        public void visit(IonNull value) throws IOException
        {
            writeAnnotations(value);
            myOut.append("null");
        }


        @Override
        public void visit(IonSexp value) throws IOException, Exception
        {
            writeAnnotations(value);

            if (value.isNullValue())
            {
                writeNull("sexp");
            }
            else if (myOptions.sexpAsList)
            {
                writeSequenceContent(value, true, '[', ',', ']');
            }
            else
            {
                writeSequenceContent(value, false, '(', ' ', ')');
            }
        }


        @Override
        public void visit(IonString value) throws IOException
        {
            writeAnnotations(value);

            if (value.isNullValue())
            {
                writeNull("string");
            }
            else
            {
                writeString(value.stringValue());
            }
        }


        @Override
        public void visit(IonStruct value) throws IOException, Exception
        {
            writeAnnotations(value);

            if (value.isNullValue())
            {
                writeNull("struct");
            }
            else
            {
                myOut.append('{');

                boolean hitOne = false;
                for (IonValue child : value)
                {
                    if (hitOne)
                    {
                        myOut.append(',');
                    }
                    hitOne = true;

                    SymbolToken sym = ((_Private_IonValue)child).getFieldNameSymbol(mySymbolTableProvider);
                    writeSymbolToken(sym);
                    myOut.append(':');
                    writeChild(child, true);
                }
                myOut.append('}');
            }
        }


        @Override
        public void visit(IonSymbol value) throws IOException
        {
            writeAnnotations(value);

            SymbolToken is = ((_Private_IonSymbol)value).symbolValue(mySymbolTableProvider);
            if (is == null)
            {
                writeNull("symbol");
            }
            else
            {
                writeSymbolToken(is);
            }
        }


        @Override
        public void visit(IonTimestamp value) throws IOException
        {
            writeAnnotations(value);

            if (value.isNullValue())
            {
                writeNull("timestamp");
            }
            else if (myOptions.timestampAsMillis)
            {
                myOut.append(Long.toString(value.getMillis()));
            }
            else
            {
                Timestamp ts = value.timestampValue();

                if (myOptions.timestampAsString)
                {
                    myOut.append('"');
                    ts.print(myOut);
                    myOut.append('"');
                }
                else
                {
                    ts.print(myOut);
                }
            }
        }
    }
}