_Private_IonTextAppender.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.impl;
import static com.amazon.ion.impl._Private_IonConstants.MAX_LONG_TEXT_SIZE;
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 com.amazon.ion.Decimal;
import com.amazon.ion.impl.Base64Encoder.TextStream;
import com.amazon.ion.impl.schubfach.DoubleToDecimal;
import com.amazon.ion.system.IonTextWriterBuilder;
import com.amazon.ion.util._Private_FastAppendable;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
/**
* NOT FOR APPLICATION USE!
* <p>
* Generic text sink that enables optimized output of both ASCII and UTF-16.
*/
public final class _Private_IonTextAppender
implements Closeable, Flushable
{
private static boolean is8bitValue(int v)
{
return (v & ~0xff) == 0;
}
private static boolean isDecimalDigit(int codePoint)
{
return (codePoint >= '0' && codePoint <= '9');
}
private static final boolean[] IDENTIFIER_START_CHAR_FLAGS;
private static final boolean[] IDENTIFIER_FOLLOW_CHAR_FLAGS;
static
{
IDENTIFIER_START_CHAR_FLAGS = new boolean[256];
IDENTIFIER_FOLLOW_CHAR_FLAGS = new boolean[256];
for (int ii='a'; ii<='z'; ii++) {
IDENTIFIER_START_CHAR_FLAGS[ii] = true;
IDENTIFIER_FOLLOW_CHAR_FLAGS[ii] = true;
}
for (int ii='A'; ii<='Z'; ii++) {
IDENTIFIER_START_CHAR_FLAGS[ii] = true;
IDENTIFIER_FOLLOW_CHAR_FLAGS[ii] = true;
}
IDENTIFIER_START_CHAR_FLAGS ['_'] = true;
IDENTIFIER_FOLLOW_CHAR_FLAGS['_'] = true;
IDENTIFIER_START_CHAR_FLAGS ['$'] = true;
IDENTIFIER_FOLLOW_CHAR_FLAGS['$'] = true;
for (int ii='0'; ii<='9'; ii++) {
IDENTIFIER_FOLLOW_CHAR_FLAGS[ii] = true;
}
}
public static boolean isIdentifierStart(int codePoint) {
return IDENTIFIER_START_CHAR_FLAGS[codePoint & 0xff] && is8bitValue(codePoint);
}
public static boolean isIdentifierPart(int codePoint) {
return IDENTIFIER_FOLLOW_CHAR_FLAGS[codePoint & 0xff] && is8bitValue(codePoint);
}
public static final boolean[] OPERATOR_CHAR_FLAGS;
static
{
final char[] operatorChars = {
'<', '>', '=', '+', '-', '*', '&', '^', '%',
'~', '/', '?', '.', ';', '!', '|', '@', '`', '#'
};
OPERATOR_CHAR_FLAGS = new boolean[256];
for (int ii=0; ii<operatorChars.length; ii++) {
char operator = operatorChars[ii];
OPERATOR_CHAR_FLAGS[operator] = true;
}
}
public static boolean isOperatorPart(int codePoint) {
return OPERATOR_CHAR_FLAGS[codePoint & 0xff] && is8bitValue(codePoint);
}
public static final String[] ZERO_PADDING =
{
"",
"0",
"00",
"000",
"0000",
"00000",
"000000",
"0000000",
};
/**
* Escapes for U+00 through U+FF, for use in double-quoted Ion strings.
* This includes escapes for all LATIN-1 code points U+80 through U+FF.
*/
private static final String[] STRING_ESCAPE_CODES;
static
{
STRING_ESCAPE_CODES = new String[256];
STRING_ESCAPE_CODES[0x00] = "\\0";
STRING_ESCAPE_CODES[0x07] = "\\a";
STRING_ESCAPE_CODES[0x08] = "\\b";
STRING_ESCAPE_CODES['\t'] = "\\t";
STRING_ESCAPE_CODES['\n'] = "\\n";
STRING_ESCAPE_CODES[0x0B] = "\\v";
STRING_ESCAPE_CODES['\f'] = "\\f";
STRING_ESCAPE_CODES['\r'] = "\\r";
STRING_ESCAPE_CODES['\\'] = "\\\\";
STRING_ESCAPE_CODES['\"'] = "\\\"";
for (int i = 1; i < 0x20; ++i) {
if (STRING_ESCAPE_CODES[i] == null) {
String s = Integer.toHexString(i);
STRING_ESCAPE_CODES[i] = "\\x" + ZERO_PADDING[2 - s.length()] + s;
}
}
for (int i = 0x7F; i < 0x100; ++i) {
String s = Integer.toHexString(i);
STRING_ESCAPE_CODES[i] = "\\x" + s;
}
}
/**
* Escapes for U+00 through U+FF, for use in triple-quoted Ion strings.
* This includes escapes for all LATIN-1 code points U+80 through U+FF.
*/
static final String[] LONG_STRING_ESCAPE_CODES;
static
{
LONG_STRING_ESCAPE_CODES = new String[256];
for (int i = 0; i < 256; ++i) {
LONG_STRING_ESCAPE_CODES[i] = STRING_ESCAPE_CODES[i];
}
LONG_STRING_ESCAPE_CODES['\n'] = null;
LONG_STRING_ESCAPE_CODES['\''] = "\\\'";
LONG_STRING_ESCAPE_CODES['\"'] = null; // Treat as normal code point for long string
}
/**
* Escapes for U+00 through U+FF, for use in single-quoted Ion symbols.
* This includes escapes for all LATIN-1 code points U+80 through U+FF.
*/
static final String[] SYMBOL_ESCAPE_CODES;
static
{
SYMBOL_ESCAPE_CODES = new String[256];
for (int i = 0; i < 256; ++i) {
SYMBOL_ESCAPE_CODES[i] = STRING_ESCAPE_CODES[i];
}
SYMBOL_ESCAPE_CODES['\''] = "\\\'";
SYMBOL_ESCAPE_CODES['\"'] = null; // Treat as normal code point for symbol.
}
/**
* Escapes for U+00 through U+FF, for use in double-quoted JSON strings.
* This includes escapes for all LATIN-1 code points U+80 through U+FF.
*/
static final String[] JSON_ESCAPE_CODES;
static
{
JSON_ESCAPE_CODES = new String[256];
JSON_ESCAPE_CODES[0x08] = "\\b";
JSON_ESCAPE_CODES['\t'] = "\\t";
JSON_ESCAPE_CODES['\n'] = "\\n";
JSON_ESCAPE_CODES['\f'] = "\\f";
JSON_ESCAPE_CODES['\r'] = "\\r";
JSON_ESCAPE_CODES['\\'] = "\\\\";
JSON_ESCAPE_CODES['\"'] = "\\\"";
// JSON requires all of these characters to be escaped.
for (int i = 0; i < 0x20; ++i) {
if (JSON_ESCAPE_CODES[i] == null) {
String s = Integer.toHexString(i);
JSON_ESCAPE_CODES[i] = "\\u" + ZERO_PADDING[4 - s.length()] + s;
}
}
for (int i = 0x7F; i < 0x100; ++i) {
String s = Integer.toHexString(i);
JSON_ESCAPE_CODES[i] = "\\u00" + s;
}
}
private static final String HEX_4_PREFIX = "\\u";
private static final String HEX_8_PREFIX = "\\U";
private static final String TRIPLE_QUOTES = "'''";
//=========================================================================
private final _Private_FastAppendable myAppendable;
private final boolean escapeNonAscii;
_Private_IonTextAppender(_Private_FastAppendable out, boolean escapeNonAscii)
{
this.myAppendable = out;
this.escapeNonAscii = escapeNonAscii;
}
public static _Private_IonTextAppender
forFastAppendable(_Private_FastAppendable out, Charset charset)
{
boolean escapeNonAscii = charset.equals(_Private_Utils.ASCII_CHARSET);
return new _Private_IonTextAppender(out, escapeNonAscii);
}
/**
* @param charset must be either {@link IonTextWriterBuilder#ASCII} or
* {@link IonTextWriterBuilder#UTF8}. When ASCII is used, all non-ASCII
* characters will be escaped. Otherwise, only select code points will be
* escaped.
*/
public static _Private_IonTextAppender forAppendable(Appendable out,
Charset charset)
{
_Private_FastAppendable fast = new AppendableFastAppendable(out);
return forFastAppendable(fast, charset);
}
/**
* Doesn't escape non-ASCII characters.
*/
public static _Private_IonTextAppender forAppendable(Appendable out)
{
_Private_FastAppendable fast = new AppendableFastAppendable(out);
boolean escapeNonAscii = false;
return new _Private_IonTextAppender(fast, escapeNonAscii);
}
/**
* @param charset must be either {@link IonTextWriterBuilder#ASCII} or
* {@link IonTextWriterBuilder#UTF8}. When ASCII is used, all non-ASCII
* characters will be escaped. Otherwise, only select code points will be
* escaped.
*/
public static _Private_IonTextAppender forOutputStream(OutputStream out,
Charset charset)
{
_Private_FastAppendable fast = new OutputStreamFastAppendable(out);
return forFastAppendable(fast, charset);
}
//=========================================================================
public void flush()
throws IOException
{
if (myAppendable instanceof Flushable)
{
((Flushable) myAppendable).flush();
}
}
public void close()
throws IOException
{
if (myAppendable instanceof Closeable)
{
((Closeable) myAppendable).close();
}
}
public void appendAscii(char c)
throws IOException
{
myAppendable.appendAscii(c);
}
public void appendAscii(CharSequence csq)
throws IOException
{
myAppendable.appendAscii(csq);
}
public void appendAscii(CharSequence csq, int start, int end)
throws IOException
{
myAppendable.appendAscii(csq, start, end);
}
public void appendUtf16(char c)
throws IOException
{
myAppendable.appendUtf16(c);
}
public void appendUtf16Surrogate(char leadSurrogate, char trailSurrogate)
throws IOException
{
myAppendable.appendUtf16Surrogate(leadSurrogate, trailSurrogate);
}
//=========================================================================
/**
* Print an Ion String type
* @param text
* @throws IOException
*/
public final void printString(CharSequence text)
throws IOException
{
if (text == null)
{
appendAscii("null.string");
}
else
{
appendAscii('"');
printCodePoints(text, STRING_ESCAPE_CODES);
appendAscii('"');
}
}
/**
* Print an Ion triple-quoted string
* @param text
* @throws IOException
*/
public final void printLongString(CharSequence text)
throws IOException
{
if (text == null)
{
appendAscii("null.string");
}
else
{
appendAscii(TRIPLE_QUOTES);
printCodePoints(text, LONG_STRING_ESCAPE_CODES);
appendAscii(TRIPLE_QUOTES);
}
}
/**
* Print a JSON string
* @param text
* @throws IOException
*/
public final void printJsonString(CharSequence text)
throws IOException
{
if (text == null)
{
appendAscii("null");
}
else
{
appendAscii('"');
printCodePoints(text, JSON_ESCAPE_CODES);
appendAscii('"');
}
}
/**
* Determines whether the given text matches one of the Ion identifier
* keywords <code>null</code>, <code>true</code>, or <code>false</code>.
* <p>
* This does <em>not</em> check for non-identifier keywords such as
* <code>null.int</code>.
*
* @param text the symbol to check.
* @return <code>true</code> if the text is an identifier keyword.
*/
public static boolean isIdentifierKeyword(CharSequence text)
{
int pos = 0;
int valuelen = text.length();
if(valuelen == 0)
{
return false;
}
boolean keyword = false;
// there has to be at least 1 character or we wouldn't be here
switch (text.charAt(pos++)) {
case '$':
if (valuelen == 1) return false;
while (pos < valuelen) {
char c = text.charAt(pos++);
if (! isDecimalDigit(c)) return false;
}
return true;
case 'f':
if (valuelen == 5 // 'f'
&& text.charAt(pos++) == 'a'
&& text.charAt(pos++) == 'l'
&& text.charAt(pos++) == 's'
&& text.charAt(pos++) == 'e'
) {
keyword = true;
}
break;
case 'n':
if (valuelen == 4 // 'n'
&& text.charAt(pos++) == 'u'
&& text.charAt(pos++) == 'l'
&& text.charAt(pos++) == 'l'
) {
keyword = true;
}
else if (valuelen == 3 // 'n'
&& text.charAt(pos++) == 'a'
&& text.charAt(pos++) == 'n'
) {
keyword = true;
}
break;
case 't':
if (valuelen == 4 // 't'
&& text.charAt(pos++) == 'r'
&& text.charAt(pos++) == 'u'
&& text.charAt(pos++) == 'e'
) {
keyword = true;
}
break;
}
return keyword;
}
/**
* Determines whether the text of a symbol requires (single) quotes.
*
* @param symbol must be a non-empty string.
* @param quoteOperators indicates whether the caller wants operators to be
* quoted; if <code>true</code> then operator symbols like <code>!=</code>
* will return <code>true</code>.
* has looser quoting requirements than other containers.
* @return <code>true</code> if the given symbol requires quoting.
*
* @throws NullPointerException
* if <code>symbol</code> is <code>null</code>.
*/
public static boolean symbolNeedsQuoting(CharSequence symbol,
boolean quoteOperators)
{
int length = symbol.length();
// 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 true;
}
char c = symbol.charAt(0);
// Surrogates are neither identifierStart nor operatorPart, so the
// first one we hit will fall through and return true.
// TODO test that
if (!quoteOperators && 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 true;
}
}
return false;
}
else if (isIdentifierStart(c))
{
for (int ii = 0; ii < length; ii++) {
c = symbol.charAt(ii);
if ((c == '\'' || c < 32 || c > 126)
|| !isIdentifierPart(c))
{
return true;
}
}
return false;
}
// quote by default
return true;
}
/**
* Print an Ion Symbol type. This method will check if symbol needs quoting
* @param text
* @throws IOException
*/
public final void printSymbol(CharSequence text)
throws IOException
{
if (text == null)
{
appendAscii("null.symbol");
}
else if (symbolNeedsQuoting(text, true)) {
appendAscii('\'');
printCodePoints(text, SYMBOL_ESCAPE_CODES);
appendAscii('\'');
}
else
{
appendAscii(text);
}
}
/**
* Print single-quoted Ion Symbol type
* @param text
* @throws IOException
*/
public final void printQuotedSymbol(CharSequence text)
throws IOException
{
if (text == null)
{
appendAscii("null.symbol");
}
else
{
appendAscii('\'');
printCodePoints(text, SYMBOL_ESCAPE_CODES);
appendAscii('\'');
}
}
private final void printCodePoints(CharSequence text, String[] escapes)
throws IOException
{
int len = text.length();
for (int i = 0; i < len; ++i)
{
// Find a span of non-escaped ASCII code points so we can write
// them as quickly as possible.
char c = 0;
int j;
for (j = i; j < len; ++j) {
c = text.charAt(j);
// The escapes array always includes U+80 through U+FF.
if (c >= 0x100 || escapes[c] != null)
{
// c is escaped and/or outside ASCII range.
if (j > i) {
appendAscii(text, i, j);
i = j;
}
break;
}
}
if (j == len) {
// we've reached the end of sequence; append it and break
appendAscii(text, i, j);
break;
}
// We've found a code point that's escaped and/or non-ASCII.
if (c < 0x80)
{
// An escaped ASCII character.
assert escapes[c] != null;
appendAscii(escapes[c]);
}
else if (c < 0x100)
{
// Non-ASCII LATIN-1; we will have an escape sequence but may
// not use it.
assert escapes[c] != null;
// Always escape the C1 control codes U+80 through U+9F.
if (escapeNonAscii || c <= 0x9F) {
appendAscii(escapes[c]);
} else {
appendUtf16(c);
}
}
else if (c < 0xD800 || c >= 0xE000)
{
// Not LATIN-1, but still in the BMP.
String s = Integer.toHexString(c);
if (escapeNonAscii) {
appendAscii(HEX_4_PREFIX);
appendAscii(ZERO_PADDING[4 - s.length()]);
appendAscii(s);
} else {
appendUtf16(c);
}
}
else if (isHighSurrogate(c))
{
// Outside the BMP! High surrogate must be followed by low.
char c2;
if (++i == len || !isLowSurrogate(c2 = text.charAt(i))) {
String message =
"text is invalid UTF-16. It contains an unmatched " +
"leading surrogate 0x" + Integer.toHexString(c) +
" at index " + (i-1);
throw new IllegalArgumentException(message);
}
if (escapeNonAscii) {
int cp = makeUnicodeScalar(c, c2);
String s = Integer.toHexString(cp);
appendAscii(HEX_8_PREFIX);
appendAscii(ZERO_PADDING[8 - s.length()]);
appendAscii(s);
} else {
appendUtf16Surrogate(c, c2);
}
}
else
{
// unmatched low surrogate
assert isLowSurrogate(c);
String message =
"text is invalid UTF-16. It contains an unmatched " +
"trailing surrogate 0x" + Integer.toHexString(c) +
" at index " + i;
throw new IllegalArgumentException(message);
}
}
}
//=========================================================================
// Numeric scalars
/** ONLY FOR USE BY {@link #printInt(long)}. */
private final char[] _fixedIntBuffer = new char[MAX_LONG_TEXT_SIZE];
public void printInt(long value)
throws IOException
{
int j = _fixedIntBuffer.length;
if (value == 0) {
_fixedIntBuffer[--j] = '0';
} else {
if (value < 0) {
while (value != 0) {
_fixedIntBuffer[--j] = (char)(0x30 - value % 10);
value /= 10;
}
_fixedIntBuffer[--j] = '-';
} else {
while (value != 0) {
_fixedIntBuffer[--j] = (char)(0x30 + value % 10);
value /= 10;
}
}
}
// Using CharBuffer avoids copying the _fixedIntBuffer into a String
appendAscii(CharBuffer.wrap(_fixedIntBuffer),
j,
_fixedIntBuffer.length);
}
public void printInt(BigInteger value)
throws IOException
{
if (value == null)
{
appendAscii("null.int");
return;
}
appendAscii(bigIntegerToString(value));
}
public void printDecimal(_Private_IonTextWriterBuilder _options,
BigDecimal value)
throws IOException
{
if (value == null)
{
appendAscii("null.decimal");
return;
}
BigInteger unscaled = value.unscaledValue();
int signum = value.signum();
if (signum < 0)
{
appendAscii('-');
unscaled = unscaled.negate();
}
else if (value instanceof Decimal
&& ((Decimal)value).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.
appendAscii('-');
}
final String unscaledText = bigIntegerToString(unscaled);
final int significantDigits = unscaledText.length();
final int scale = value.scale();
final int exponent = -scale;
if (_options._decimal_as_float)
{
appendAscii(unscaledText);
appendAscii('e');
appendAscii(Integer.toString(exponent));
}
else if (exponent == 0)
{
appendAscii(unscaledText);
appendAscii('.');
}
else if (exponent < 0)
{
// Avoid printing small negative exponents using a heuristic
// adapted from http://speleotrove.com/decimal/daconvs.html
final int adjustedExponent = significantDigits - 1 - scale;
if (adjustedExponent >= 0)
{
int wholeDigits = significantDigits - scale;
appendAscii(unscaledText, 0, wholeDigits);
appendAscii('.');
appendAscii(unscaledText, wholeDigits,
significantDigits);
}
else if (adjustedExponent >= -6)
{
appendAscii("0.");
appendAscii("00000", 0, scale - significantDigits);
appendAscii(unscaledText);
}
else
{
appendAscii(unscaledText);
appendAscii("d-");
appendAscii(Integer.toString(scale));
}
}
else // (exponent > 0)
{
// We cannot move the decimal point to the right, adding
// rightmost zeros, because that would alter the precision.
appendAscii(unscaledText);
appendAscii('d');
appendAscii(Integer.toString(exponent));
}
}
public void printFloat(_Private_IonTextWriterBuilder _options, double value)
throws IOException
{
// shortcut zero cases
if (value == 0.0)
{
if (Double.compare(value, 0d) == 0) // Only matches positive zero
{
appendAscii("0e0");
}
else
{
appendAscii("-0e0");
}
}
else if (Double.isNaN(value))
{
if (_options._float_nan_and_inf_as_null) {
appendAscii("null");
} else {
appendAscii("nan");
}
}
else if (value == Double.POSITIVE_INFINITY)
{
if (_options._float_nan_and_inf_as_null) {
appendAscii("null");
} else {
appendAscii("+inf");
}
}
else if (value == Double.NEGATIVE_INFINITY)
{
if (_options._float_nan_and_inf_as_null) {
appendAscii("null");
} else {
appendAscii("-inf");
}
}
else
{
String str = DoubleToDecimal.toString(value);
appendAscii(str);
}
}
public void printFloat(_Private_IonTextWriterBuilder _options, Double value)
throws IOException
{
if (value == null)
{
appendAscii("null.float");
}
else
{
printFloat(_options, value.doubleValue());
}
}
//=========================================================================
// LOBs
public void printBlob(_Private_IonTextWriterBuilder _options,
byte[] value, int start, int len)
throws IOException
{
if (value == null)
{
appendAscii("null.blob");
return;
}
@SuppressWarnings("resource")
TextStream ts =
new TextStream(new ByteArrayInputStream(value, start, len));
// base64 encoding is 6 bits per char so
// it evens out at 3 bytes in 4 characters
char[] buf = new char[_options.isPrettyPrintOn() ? 80 : 400];
CharBuffer cb = CharBuffer.wrap(buf);
if (_options._blob_as_string)
{
appendAscii('"');
}
else
{
appendAscii("{{");
if (_options.isPrettyPrintOn())
{
appendAscii(' ');
}
}
for (;;)
{
// TODO is it better to fill up the CharBuffer before outputting?
int clen = ts.read(buf, 0, buf.length);
if (clen < 1) break;
appendAscii(cb, 0, clen);
}
if (_options._blob_as_string)
{
appendAscii('"');
}
else
{
if (_options.isPrettyPrintOn())
{
appendAscii(' ');
}
appendAscii("}}");
}
}
private void printClobBytes(byte[] value, int start, int end,
String[] escapes)
throws IOException
{
for (int i = start; i < end; i++) {
char c = (char)(value[i] & 0xff);
String escapedByte = escapes[c];
if (escapedByte != null) {
appendAscii(escapedByte);
} else {
appendAscii(c);
}
}
}
public void printClob(_Private_IonTextWriterBuilder _options,
byte[] value, int start, int len)
throws IOException
{
if (value == null)
{
appendAscii("null.clob");
return;
}
final boolean json =
_options._clob_as_string && _options._string_as_json;
final int threshold = _options.getLongStringThreshold();
final boolean longString = (0 < threshold && threshold < value.length);
if (!_options._clob_as_string)
{
appendAscii("{{");
if (_options.isPrettyPrintOn())
{
appendAscii(' ');
}
}
if (json)
{
appendAscii('"');
printClobBytes(value, start, start + len, JSON_ESCAPE_CODES);
appendAscii('"');
}
else if (longString)
{
// This may escape more often than is necessary, but doing it
// minimally is very tricky. Must be sure to account for
// quotes at the end of the content.
// TODO Account for NL versus CR+NL streams
appendAscii(TRIPLE_QUOTES);
printClobBytes(value, start, start + len, LONG_STRING_ESCAPE_CODES);
appendAscii(TRIPLE_QUOTES);
}
else
{
appendAscii('"');
printClobBytes(value, start, start + len, STRING_ESCAPE_CODES);
appendAscii('"');
}
if (! _options._clob_as_string)
{
if (_options.isPrettyPrintOn())
{
appendAscii(' ');
}
appendAscii("}}");
}
}
/**
* Convert {@link BigInteger} to a {@link String}.
* <p>
* The current implementation of {@link BigInteger#toString()} is suboptimal
* speed-wise, as it uses non-native division for arbitrary-sized integers
* to convert the binary representation to a string.
* This is inefficient for integers that can fit into a {@link Long}, where
* native division can be used to convert the long to its string representation,
* which is currently implemented in {@link Long#toString()}.
* <p>
* This method delegates to {@link Long#toString()} if it's possible to
* do so, which results in a speedup from 2x to 5x.
*
* @param value the integer to convert
* @return the string representation in base 10
*/
private String bigIntegerToString(final BigInteger value)
{
if (value.bitLength() >= 64) {
// if it's out of long range, the only way is through toString()
return value.toString();
} else {
return Long.toString(value.longValue());
}
}
}