IonEncoder_1_1.java
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package com.amazon.ion.impl.bin;
import com.amazon.ion.Decimal;
import com.amazon.ion.IonType;
import com.amazon.ion.Timestamp;
import com.amazon.ion.impl.bin.utf8.Utf8StringEncoder;
import com.amazon.ion.impl.bin.utf8.Utf8StringEncoderPool;
import java.math.BigDecimal;
import java.math.BigInteger;
import static com.amazon.ion.impl.bin.Ion_1_1_Constants.*;
import static java.lang.Double.doubleToRawLongBits;
import static java.lang.Float.floatToIntBits;
/**
* Provides functions for writing various Ion values to a WriteBuffer.
*
* This class can be subsumed by IonRawBinaryWriter_1_1, when it is created.
*/
public class IonEncoder_1_1 {
/**
* Writes an Ion Null value to the given WriteBuffer.
* @return the number of bytes written
*/
public static int writeNullValue(WriteBuffer buffer, final IonType ionType) {
if (ionType == IonType.NULL) {
buffer.writeByte(OpCodes.NULL_UNTYPED);
return 1;
}
buffer.writeByte(OpCodes.NULL_TYPED);
switch (ionType) {
case BOOL:
buffer.writeByte((byte) 0x00);
break;
case INT:
buffer.writeByte((byte) 0x01);
break;
case FLOAT:
buffer.writeByte((byte) 0x02);
break;
case DECIMAL:
buffer.writeByte((byte) 0x03);
break;
case TIMESTAMP:
buffer.writeByte((byte) 0x04);
break;
case STRING:
buffer.writeByte((byte) 0x05);
break;
case SYMBOL:
buffer.writeByte((byte) 0x06);
break;
case BLOB:
buffer.writeByte((byte) 0x07);
break;
case CLOB:
buffer.writeByte((byte) 0x08);
break;
case LIST:
buffer.writeByte((byte) 0x09);
break;
case SEXP:
buffer.writeByte((byte) 0x0A);
break;
case STRUCT:
buffer.writeByte((byte) 0x0B);
break;
case DATAGRAM:
throw new IllegalArgumentException("Cannot write a null datagram");
}
return 2;
}
/**
* Writes an Ion Bool value to the given WriteBuffer.
* @return the number of bytes written
*/
public static int writeBoolValue(WriteBuffer buffer, final boolean value) {
if (value) {
buffer.writeByte(OpCodes.BOOLEAN_TRUE);
} else {
buffer.writeByte(OpCodes.BOOLEAN_FALSE);
}
return 1;
}
/**
* Writes an Ion Integer value to the given WriteBuffer.
* @return the number of bytes written
*/
public static int writeIntValue(WriteBuffer buffer, final long value) {
if (value == 0) {
buffer.writeByte(OpCodes.INTEGER_ZERO_LENGTH);
return 1;
}
int length = WriteBuffer.fixedIntLength(value);
buffer.writeByte((byte) (OpCodes.INTEGER_ZERO_LENGTH + length));
buffer.writeFixedInt(value);
return 1 + length;
}
private static final BigInteger BIG_INT_LONG_MAX_VALUE = BigInteger.valueOf(Long.MAX_VALUE);
private static final BigInteger BIG_INT_LONG_MIN_VALUE = BigInteger.valueOf(Long.MIN_VALUE);
/**
* Writes an Ion Integer value to the given WriteBuffer.
* @return the number of bytes written
*/
public static int writeIntValue(WriteBuffer buffer, final BigInteger value) {
if (value == null) {
return writeNullValue(buffer, IonType.INT);
}
if (value.compareTo(BIG_INT_LONG_MIN_VALUE) >= 0 && value.compareTo(BIG_INT_LONG_MAX_VALUE) <= 0) {
return writeIntValue(buffer, value.longValue());
}
buffer.writeByte(OpCodes.VARIABLE_LENGTH_INTEGER);
byte[] intBytes = value.toByteArray();
int totalBytes = 1 + intBytes.length + buffer.writeFlexUInt(intBytes.length);
for (int i = intBytes.length; i > 0; i--) {
buffer.writeByte(intBytes[i-1]);
}
return totalBytes;
}
/**
* Writes a float to the given WriteBuffer using the Ion 1.1 encoding for Ion Floats.
* @return the number of bytes written
*/
public static int writeFloat(WriteBuffer buffer, final float value) {
// TODO: Optimization to write a 16 bit float for non-finite and possibly other values
if (value == 0.0) {
buffer.writeByte(OpCodes.FLOAT_ZERO_LENGTH);
return 1;
} else {
buffer.writeByte(OpCodes.FLOAT_32);
buffer.writeUInt32(floatToIntBits(value));
return 5;
}
}
/**
* Writes a double to the given WriteBuffer using the Ion 1.1 encoding for Ion Floats.
* @return the number of bytes written
*/
public static int writeFloat(WriteBuffer buffer, final double value) {
// TODO: Optimization to write a 16 bit float for non-finite and possibly other values
if (value == 0.0) {
buffer.writeByte(OpCodes.FLOAT_ZERO_LENGTH);
return 1;
} else if (!Double.isFinite(value) || value == (float) value) {
buffer.writeByte(OpCodes.FLOAT_32);
buffer.writeUInt32(floatToIntBits((float) value));
return 5;
} else {
buffer.writeByte(OpCodes.FLOAT_64);
buffer.writeUInt64(doubleToRawLongBits(value));
return 9;
}
}
public static int writeDecimalValue(WriteBuffer buffer, final BigDecimal value) {
if (value == null) {
return writeNullValue(buffer, IonType.DECIMAL);
}
int exponent = -value.scale();
if (BigDecimal.ZERO.compareTo(value) == 0 && !Decimal.isNegativeZero(value)) {
if (exponent == 0) {
buffer.writeByte(OpCodes.DECIMAL_ZERO_LENGTH);
return 1;
} else {
// A decimal with a coefficient of +0 is encoded using opcode 6F.
// The opcode is followed by a FlexInt representing the exponent.
buffer.writeByte(OpCodes.POSITIVE_ZERO_DECIMAL);
return 1 + buffer.writeFlexInt(exponent);
}
}
BigInteger coefficient = value.unscaledValue();
int numCoefficientBytes = WriteBuffer.flexIntLength(coefficient);
int numExponentBytes = 0;
if (exponent != 0) {
numExponentBytes = WriteBuffer.fixedIntLength(exponent);
}
int opCodeAndLengthBytes = 1;
if (numExponentBytes + numCoefficientBytes < 15) {
int opCode = OpCodes.DECIMAL_ZERO_LENGTH + numExponentBytes + numCoefficientBytes;
buffer.writeByte((byte) opCode);
} else {
// Decimal values that require more than 14 bytes can be encoded using the variable-length decimal opcode: 0xF6.
buffer.writeByte(OpCodes.VARIABLE_LENGTH_DECIMAL);
opCodeAndLengthBytes += buffer.writeFlexUInt(numExponentBytes + numCoefficientBytes);
}
buffer.writeFlexInt(coefficient);
if (exponent != 0) {
buffer.writeFixedInt(exponent);
}
return opCodeAndLengthBytes + numCoefficientBytes + numExponentBytes;
}
/**
* Writes a Timestamp to the given WriteBuffer using the Ion 1.1 encoding for Ion Timestamps.
* @return the number of bytes written
*/
public static int writeTimestampValue(WriteBuffer buffer, Timestamp value) {
if (value == null) {
return writeNullValue(buffer, IonType.TIMESTAMP);
}
// Timestamps may be encoded using the short form if they meet certain conditions.
// Condition 1: The year is between 1970 and 2097.
if (value.getYear() < 1970 || value.getYear() > 2097) {
return writeLongFormTimestampValue(buffer, value);
}
// If the precision is year, month, or day, we can skip the remaining checks.
if (!value.getPrecision().includes(Timestamp.Precision.MINUTE)) {
return writeShortFormTimestampValue(buffer, value);
}
// Condition 2: The fractional seconds are a common precision.
if (value.getZFractionalSecond() != null) {
int secondsScale = value.getZFractionalSecond().scale();
if (secondsScale != 0 && secondsScale != 3 && secondsScale != 6 && secondsScale != 9) {
return writeLongFormTimestampValue(buffer, value);
}
}
// Condition 3: The local offset is either UTC, unknown, or falls between -14:00 to +14:00 and is divisible by 15 minutes.
Integer offset = value.getLocalOffset();
if (offset != null && (offset < -14 * 60 || offset > 14 * 60 || offset % 15 != 0)) {
return writeLongFormTimestampValue(buffer, value);
}
return writeShortFormTimestampValue(buffer, value);
}
/**
* Writes a short-form timestamp.
* Value cannot be null.
* If calling from outside this class, use writeTimestampValue instead.
*/
private static int writeShortFormTimestampValue(WriteBuffer buffer, Timestamp value) {
long bits = (value.getYear() - 1970L);
if (value.getPrecision() == Timestamp.Precision.YEAR) {
buffer.writeByte(OpCodes.TIMESTAMP_YEAR_PRECISION);
buffer.writeFixedIntOrUInt(bits, 1);
return 2;
}
bits |= ((long) value.getMonth()) << S_TIMESTAMP_MONTH_BIT_OFFSET;
if (value.getPrecision() == Timestamp.Precision.MONTH) {
buffer.writeByte(OpCodes.TIMESTAMP_MONTH_PRECISION);
buffer.writeFixedIntOrUInt(bits, 2);
return 3;
}
bits |= ((long) value.getDay()) << S_TIMESTAMP_DAY_BIT_OFFSET;
if (value.getPrecision() == Timestamp.Precision.DAY) {
buffer.writeByte(OpCodes.TIMESTAMP_DAY_PRECISION);
buffer.writeFixedIntOrUInt(bits, 2);
return 3;
}
bits |= ((long) value.getHour()) << S_TIMESTAMP_HOUR_BIT_OFFSET;
bits |= ((long) value.getMinute()) << S_TIMESTAMP_MINUTE_BIT_OFFSET;
if (value.getLocalOffset() == null || value.getLocalOffset() == 0) {
if (value.getLocalOffset() != null) {
bits |= S_U_TIMESTAMP_UTC_FLAG;
}
if (value.getPrecision() == Timestamp.Precision.MINUTE) {
buffer.writeByte(OpCodes.TIMESTAMP_MINUTE_PRECISION);
buffer.writeFixedIntOrUInt(bits, 4);
return 5;
}
bits |= ((long) value.getSecond()) << S_U_TIMESTAMP_SECOND_BIT_OFFSET;
int secondsScale = 0;
if (value.getZFractionalSecond() != null) {
secondsScale = value.getZFractionalSecond().scale();
}
if (secondsScale != 0) {
long fractionalSeconds = value.getZFractionalSecond().unscaledValue().longValue();
bits |= fractionalSeconds << S_U_TIMESTAMP_FRACTION_BIT_OFFSET;
}
switch (secondsScale) {
case 0:
buffer.writeByte(OpCodes.TIMESTAMP_SECOND_PRECISION);
buffer.writeFixedIntOrUInt(bits, 5);
return 6;
case 3:
buffer.writeByte(OpCodes.TIMESTAMP_MILLIS_PRECISION);
buffer.writeFixedIntOrUInt(bits, 6);
return 7;
case 6:
buffer.writeByte(OpCodes.TIMESTAMP_MICROS_PRECISION);
buffer.writeFixedIntOrUInt(bits, 7);
return 8;
case 9:
buffer.writeByte(OpCodes.TIMESTAMP_NANOS_PRECISION);
buffer.writeFixedIntOrUInt(bits, 8);
return 9;
default:
throw new IllegalStateException("This is unreachable!");
}
} else {
long localOffset = (value.getLocalOffset().longValue() / 15) + (14 * 4);
bits |= (localOffset & LEAST_SIGNIFICANT_7_BITS) << S_O_TIMESTAMP_OFFSET_BIT_OFFSET;
if (value.getPrecision() == Timestamp.Precision.MINUTE) {
buffer.writeByte(OpCodes.TIMESTAMP_MINUTE_PRECISION_WITH_OFFSET);
buffer.writeFixedIntOrUInt(bits, 5);
return 6;
}
bits |= ((long) value.getSecond()) << S_O_TIMESTAMP_SECOND_BIT_OFFSET;
// The fractional seconds bits will be put into a separate long because we need nine bytes total
// if there are nanoseconds (which is too much for one long) and the boundary between the seconds
// and fractional seconds subfields conveniently aligns with a byte boundary.
long fractionBits = 0;
int secondsScale = 0;
if (value.getZFractionalSecond() != null) {
secondsScale = value.getZFractionalSecond().scale();
}
if (secondsScale != 0) {
fractionBits = value.getZFractionalSecond().unscaledValue().longValue();
}
switch (secondsScale) {
case 0:
buffer.writeByte(OpCodes.TIMESTAMP_SECOND_PRECISION_WITH_OFFSET);
buffer.writeFixedIntOrUInt(bits, 5);
return 6;
case 3:
buffer.writeByte(OpCodes.TIMESTAMP_MILLIS_PRECISION_WITH_OFFSET);
buffer.writeFixedIntOrUInt(bits, 5);
buffer.writeFixedIntOrUInt(fractionBits, 2);
return 8;
case 6:
buffer.writeByte(OpCodes.TIMESTAMP_MICROS_PRECISION_WITH_OFFSET);
buffer.writeFixedIntOrUInt(bits, 5);
buffer.writeFixedIntOrUInt(fractionBits, 3);
return 9;
case 9:
buffer.writeByte(OpCodes.TIMESTAMP_NANOS_PRECISION_WITH_OFFSET);
buffer.writeFixedIntOrUInt(bits, 5);
buffer.writeFixedIntOrUInt(fractionBits, 4);
return 10;
default:
throw new IllegalStateException("This is unreachable!");
}
}
}
/**
* Writes a long-form timestamp.
* Value may not be null.
* Only visible for testing. If calling from outside this class, use writeTimestampValue instead.
*/
static int writeLongFormTimestampValue(WriteBuffer buffer, Timestamp value) {
buffer.writeByte(OpCodes.VARIABLE_LENGTH_TIMESTAMP);
long bits = value.getYear();
if (value.getPrecision() == Timestamp.Precision.YEAR) {
buffer.writeFlexUInt(2);
buffer.writeFixedIntOrUInt(bits, 2);
return 4; // OpCode + FlexUInt + 2 bytes data
}
bits |= ((long) value.getMonth()) << L_TIMESTAMP_MONTH_BIT_OFFSET;
if (value.getPrecision() == Timestamp.Precision.MONTH) {
buffer.writeFlexUInt(3);
buffer.writeFixedIntOrUInt(bits, 3);
return 5; // OpCode + FlexUInt + 3 bytes data
}
bits |= ((long) value.getDay()) << L_TIMESTAMP_DAY_BIT_OFFSET;
if (value.getPrecision() == Timestamp.Precision.DAY) {
buffer.writeFlexUInt(3);
buffer.writeFixedIntOrUInt(bits, 3);
return 5; // OpCode + FlexUInt + 3 bytes data
}
bits |= ((long) value.getHour()) << L_TIMESTAMP_HOUR_BIT_OFFSET;
bits |= ((long) value.getMinute()) << L_TIMESTAMP_MINUTE_BIT_OFFSET;
long localOffsetValue = L_TIMESTAMP_UNKNOWN_OFFSET_VALUE;
if (value.getLocalOffset() != null) {
localOffsetValue = value.getLocalOffset() + (24 * 60);
}
bits |= localOffsetValue << L_TIMESTAMP_OFFSET_BIT_OFFSET;
if (value.getPrecision() == Timestamp.Precision.MINUTE) {
buffer.writeFlexUInt(6);
buffer.writeFixedIntOrUInt(bits, 6);
return 8; // OpCode + FlexUInt + 6 bytes data
}
bits |= ((long) value.getSecond()) << L_TIMESTAMP_SECOND_BIT_OFFSET;
int secondsScale = 0;
if (value.getZFractionalSecond() != null) {
secondsScale = value.getZFractionalSecond().scale();
}
if (secondsScale == 0) {
buffer.writeFlexUInt(7);
buffer.writeFixedIntOrUInt(bits, 7);
return 9; // OpCode + FlexUInt + 7 bytes data
}
BigDecimal fractionalSeconds = value.getZFractionalSecond();
BigInteger coefficient = fractionalSeconds.unscaledValue();
long exponent = fractionalSeconds.scale();
int numCoefficientBytes = WriteBuffer.flexUIntLength(coefficient);
int numExponentBytes = WriteBuffer.fixedUIntLength(exponent);
// Years-seconds data (7 bytes) + fraction coefficient + fraction exponent
int dataLength = 7 + numCoefficientBytes + numExponentBytes;
buffer.writeFlexUInt(dataLength);
buffer.writeFixedIntOrUInt(bits, 7);
buffer.writeFlexUInt(coefficient);
buffer.writeFixedUInt(exponent);
// OpCode + FlexUInt length + dataLength
return 1 + WriteBuffer.flexUIntLength(dataLength) + dataLength;
}
/**
* Writes a String to the given WriteBuffer using the Ion 1.1 encoding for Ion Strings.
* @return the number of bytes written
*/
public static int writeStringValue(WriteBuffer buffer, String value) {
return writeInlineText(buffer, value, IonType.STRING, OpCodes.STRING_ZERO_LENGTH, OpCodes.VARIABLE_LENGTH_STRING);
}
/**
* Writes an inline Symbol to the given WriteBuffer using the Ion 1.1 encoding for Ion Symbols.
* @return the number of bytes written
*/
public static int writeSymbolValue(WriteBuffer buffer, String value) {
return writeInlineText(buffer, value, IonType.SYMBOL, OpCodes.INLINE_SYMBOL_ZERO_LENGTH, OpCodes.VARIABLE_LENGTH_INLINE_SYMBOL);
}
private static int writeInlineText(WriteBuffer buffer, String value, IonType type, byte zeroLengthOpCode, byte variableLengthOpCode) {
if (value == null) {
return writeNullValue(buffer, type);
}
// TODO: When merging into the Ion 1.1 raw writer, keep a single instance of the Utf8StringEncoder
// instead of fetching one on every call.
Utf8StringEncoder.Result encoderResult = Utf8StringEncoderPool.getInstance().getOrCreate().encode(value);
byte[] utf8Buffer = encoderResult.getBuffer();
int numValueBytes = encoderResult.getEncodedLength();
int numLengthBytes = 0;
if (numValueBytes <= 0xF) {
buffer.writeByte((byte)(zeroLengthOpCode | numValueBytes));
} else {
buffer.writeByte(variableLengthOpCode);
numLengthBytes = buffer.writeFlexUInt(numValueBytes);
}
buffer.writeBytes(utf8Buffer, 0, numValueBytes);
return 1 + numLengthBytes + numValueBytes;
}
/**
* Writes an interned Symbol's address to the given WriteBuffer using the Ion 1.1 encoding for Ion Symbols.
* @return the number of bytes written
*
* TODO: Do we need to support Symbol Addresses greater than Long.MAX_VALUE?
*/
public static int writeSymbolValue(WriteBuffer buffer, long value) {
if (value < 0) {
throw new IllegalArgumentException("Symbol Address cannot be negative; was: " + value);
} else if (value < FIRST_2_BYTE_SYMBOL_ADDRESS) {
buffer.writeByte(OpCodes.SYMBOL_ADDRESS_1_BYTE);
buffer.writeFixedUInt(value);
return 2;
} else if (value < FIRST_MANY_BYTE_SYMBOL_ADDRESS) {
buffer.writeByte(OpCodes.SYMBOL_ADDRESS_2_BYTES);
buffer.writeFixedIntOrUInt(value - FIRST_2_BYTE_SYMBOL_ADDRESS, 2);
return 3;
} else {
buffer.writeByte(OpCodes.SYMBOL_ADDRESS_MANY_BYTES);
int addressBytes = buffer.writeFlexUInt(value - FIRST_MANY_BYTE_SYMBOL_ADDRESS);
return 1 + addressBytes;
}
}
/**
* Writes a Blob to the given WriteBuffer using the Ion 1.1 encoding for Ion Blobs.
* @return the number of bytes written
*/
public static int writeBlobValue(WriteBuffer buffer, byte[] value) {
if (value == null) {
return writeNullValue(buffer, IonType.BLOB);
}
buffer.writeByte(OpCodes.VARIABLE_LENGTH_BLOB);
int numLengthBytes = buffer.writeFlexUInt(value.length);
buffer.writeBytes(value);
return 1 + numLengthBytes + value.length;
}
/**
* Writes a Clob to the given WriteBuffer using the Ion 1.1 encoding for Ion Clobs.
* @return the number of bytes written
*/
public static int writeClobValue(WriteBuffer buffer, byte[] value) {
if (value == null) {
return writeNullValue(buffer, IonType.CLOB);
}
buffer.writeByte(OpCodes.VARIABLE_LENGTH_CLOB);
int numLengthBytes = buffer.writeFlexUInt(value.length);
buffer.writeBytes(value);
return 1 + numLengthBytes + value.length;
}
// TODO: Implement FlexSym Annotations
/**
* Writes annotations using the given symbol addresses.
*/
public static int writeAnnotations(WriteBuffer buffer, long[] annotations) {
if (annotations == null || annotations.length == 0) {
return 0;
}
if (annotations.length == 1) {
buffer.writeByte(OpCodes.ANNOTATIONS_1_SYMBOL_ADDRESS);
int numAddressBytes = buffer.writeFlexUInt(annotations[0]);
return 1 + numAddressBytes;
} else if (annotations.length == 2) {
buffer.writeByte(OpCodes.ANNOTATIONS_2_SYMBOL_ADDRESS);
int numAddressBytes = buffer.writeFlexUInt(annotations[0]);
numAddressBytes += buffer.writeFlexUInt(annotations[1]);
return 1 + numAddressBytes;
} else {
int numAddressBytes = 0;
for (long ann : annotations) {
numAddressBytes += WriteBuffer.flexUIntLength(ann);
}
buffer.writeByte(OpCodes.ANNOTATIONS_MANY_SYMBOL_ADDRESS);
int numLengthBytes = buffer.writeFlexUInt(numAddressBytes);
for (long ann : annotations) {
buffer.writeFlexUInt(ann);
}
return 1 + numLengthBytes + numAddressBytes;
}
}
}