All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.amazon.ion.impl.IonReaderBinaryIncremental Maven / Gradle / Ivy

There is a newer version: 1.11.9
Show newest version
package com.amazon.ion.impl;

import com.amazon.ion.Decimal;
import com.amazon.ion.IntegerSize;
import com.amazon.ion.IonBufferConfiguration;
import com.amazon.ion.IonCatalog;
import com.amazon.ion.IonException;
import com.amazon.ion.IonReader;
import com.amazon.ion.IonStruct;
import com.amazon.ion.IonType;
import com.amazon.ion.IonWriter;
import com.amazon.ion.ReadOnlyValueException;
import com.amazon.ion.SymbolTable;
import com.amazon.ion.SymbolToken;
import com.amazon.ion.Timestamp;
import com.amazon.ion.UnknownSymbolException;
import com.amazon.ion.ValueFactory;
import com.amazon.ion.impl.bin.IntList;
import com.amazon.ion.system.IonReaderBuilder;
import com.amazon.ion.impl.bin.utf8.Utf8StringDecoder;
import com.amazon.ion.impl.bin.utf8.Utf8StringDecoderPool;
import com.amazon.ion.system.SimpleCatalog;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteBuffer;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * 

* This implementation differs from the existing non-incremental binary reader implementation in that if * {@link IonReader#next()} returns {@code null} at the top-level, it indicates that there is not (yet) enough data in * the stream to complete a top-level value. The user may wait for more data to become available in the stream and * call {@link IonReader#next()} again to continue reading. Unlike the non-incremental reader, the incremental reader * will never throw an exception due to unexpected EOF during {@code next()}. If, however, {@link IonReader#close()} is * called when an incomplete value is buffered, an {@link IonException} will be raised. *

*

* Although the incremental binary reader implementation provides performance superior to the non-incremental reader * implementation for both incremental and non-incremental use cases, there is one caveat: the incremental * implementation must be able to buffer an entire top-level value and any preceding system values (Ion version * marker(s) and symbol table(s)) in memory. This means that each value and preceding system values must be no larger * than any of the following: *

    *
  • The configured maximum buffer size of the {@link IonBufferConfiguration}.
  • *
  • The memory available to the JVM.
  • *
  • 2GB, because the buffer is held in a Java {@code byte[]}, which is indexed by an {@code int}.
  • *
* This will not be a problem for the vast majority of Ion streams, as it is * rare for a single top-level value or symbol table to exceed a few megabytes in size. However, if the size of the * stream's values risk exceeding the available memory, then this implementation must not be used. *

*

* To enable this implementation, use {@code IonReaderBuilder.withIncrementalReadingEnabled(true)}. *

*/ class IonReaderBinaryIncremental implements IonReader, _Private_ReaderWriter, _Private_IncrementalReader { /* * Potential future enhancements: * - Split this implementation into a user-level reader and a system-level reader, like the existing implementation. * This allows this implementation to be used when the user requests a system reader. * - Do not require buffering an entire top-level value. This would be a pretty major overhaul. It may be possible * to implement using different buffers for each depth. Doing this may also make it possible to avoid buffering * a value (at any depth) until stepIn() or *Value() is called on it, enabling faster skip-scanning. * - Allow for this implementation to produce the same non-incremental behavior as the old implementation; namely, * that running out of data during next() would raise an IonException. See the note in the implementation of * close() below. Implementing this bullet and the previous two bullets would allow us to remove the old binary * IonReader implementation. * - Add a builder/constructor option that uses a user-provided byte[] directly. This would allow data to be read * in-place without the need to copy to a separate buffer. Non-incremental behavior (as described in the previous * bullet) is likely a requirement of this feature. * - System symbol table configuration needs to be generalized to support future Ion versions. See the constructor, * resetSymbolTable(), and resetImports(). * - When accessed via an iterator, annotations can be parsed incrementally instead of parsing the entire sequence * up-front. * - Provide users the option to spawn a thread that pre-buffers the next value. There would be two buffers: one * for the user thread, and one for the pre-fetching thread. They are swapped every time the user calls next(). */ /** * Holds the information that the binary reader must keep track of for containers at any depth. */ private static class ContainerInfo { /** * The container's type. */ private IonType type; /** * The byte position of the end of the container. */ private int endPosition; } /** * The standard {@link IonBufferConfiguration}. This will be used unless the user chooses custom settings. */ private static final IonBufferConfiguration STANDARD_BUFFER_CONFIGURATION = IonBufferConfiguration.Builder.standard().build(); // Constructs ContainerInfo instances. private static final _Private_RecyclingStack.ElementFactory CONTAINER_INFO_FACTORY = new _Private_RecyclingStack.ElementFactory() { @Override public ContainerInfo newElement() { return new ContainerInfo(); } }; // Symbol IDs for symbols contained in the system symbol table. private static class SystemSymbolIDs { // The system symbol table SID for the text "$ion_symbol_table". private static final int ION_SYMBOL_TABLE_ID = 3; // The system symbol table SID for the text "name". private static final int NAME_ID = 4; // The system symbol table SID for the text "version". private static final int VERSION_ID = 5; // The system symbol table SID for the text "imports". private static final int IMPORTS_ID = 6; // The system symbol table SID for the text "symbols". private static final int SYMBOLS_ID = 7; // The system symbol table SID for the text "max_id". private static final int MAX_ID_ID = 8; } /** * @param value a non-negative number. * @return the exponent of the next power of two greater than the given number. */ private static int logBase2(int value) { return 32 - Integer.numberOfLeadingZeros(value == 0 ? 0 : value - 1); } /** * Cache of configurations for fixed-sized streams. FIXED_SIZE_CONFIGURATIONS[i] returns a configuration with * buffer size max(8, 2^i). Retrieve a configuration large enough for a given size using * FIXED_SIZE_CONFIGURATIONS(logBase2(size)). Only supports sizes less than or equal to * STANDARD_BUFFER_CONFIGURATION.getInitialBufferSize(). */ private static final IonBufferConfiguration[] FIXED_SIZE_CONFIGURATIONS; static { int maxBufferSizeExponent = logBase2(STANDARD_BUFFER_CONFIGURATION.getInitialBufferSize()); FIXED_SIZE_CONFIGURATIONS = new IonBufferConfiguration[maxBufferSizeExponent + 1]; for (int i = 0; i <= maxBufferSizeExponent; i++) { // Create a buffer configuration for buffers of size 2^i. The minimum size is 8: the smallest power of two // larger than the minimum buffer size allowed. int size = Math.max(8, (int) Math.pow(2, i)); FIXED_SIZE_CONFIGURATIONS[i] = IonBufferConfiguration.Builder.from(STANDARD_BUFFER_CONFIGURATION) .withInitialBufferSize(size) .withMaximumBufferSize(size) .build(); } } // The final byte of the binary IVM. private static final int IVM_FINAL_BYTE = 0xEA; // Isolates the highest bit in a byte. private static final int HIGHEST_BIT_BITMASK = 0x80; // Isolates the lowest seven bits in a byte. private static final int LOWER_SEVEN_BITS_BITMASK = 0x7F; // Isolates the lowest six bits in a byte. private static final int LOWER_SIX_BITS_BITMASK = 0x3F; // The number of significant bits in each UInt byte. private static final int VALUE_BITS_PER_UINT_BYTE = 8; // The number of significant bits in each VarUInt byte. private static final int VALUE_BITS_PER_VARUINT_BYTE = 7; // An IonCatalog containing zero shared symbol tables. private static final IonCatalog EMPTY_CATALOG = new SimpleCatalog(); // Initial capacity of the stack used to hold ContainerInfo. Each additional level of nesting in the data requires // a new ContainerInfo. Depths greater than 8 will be rare. private static final int CONTAINER_STACK_INITIAL_CAPACITY = 8; // Initial capacity of the ArrayList used to hold the symbol IDs of the annotations on the current value. private static final int ANNOTATIONS_LIST_INITIAL_CAPACITY = 8; // Initial capacity of the ArrayList used to hold the text in the current symbol table. private static final int SYMBOLS_LIST_INITIAL_CAPACITY = 128; // Single byte negative zero, represented as a VarInt. Often used in timestamp encodings to indicate unknown local // offset. private static final int VAR_INT_NEGATIVE_ZERO = 0xC0; // The number of bytes occupied by a Java int. private static final int INT_SIZE_IN_BYTES = 4; // The number of bytes occupied by a Java long. private static final int LONG_SIZE_IN_BYTES = 8; // The smallest negative 8-byte integer that can fit in a long is -0x80_00_00_00_00_00_00_00. private static final int MOST_SIGNIFICANT_BYTE_OF_MIN_LONG = 0x80; // The largest positive 8-byte integer that can fit in a long is 0x7F_FF_FF_FF_FF_FF_FF_FF. private static final int MOST_SIGNIFICANT_BYTE_OF_MAX_LONG = 0x7F; // The second-most significant bit in the most significant byte of a VarInt is the sign. private static final int VAR_INT_SIGN_BITMASK = 0x40; // 32-bit floats must declare length 4. private static final int FLOAT_32_BYTE_LENGTH = 4; // The imports for Ion 1.0 data with no shared user imports. private static final LocalSymbolTableImports ION_1_0_IMPORTS = new LocalSymbolTableImports(SharedSymbolTable.getSystemSymbolTable(1)); // The InputStream that provides the binary Ion data. private final InputStream inputStream; // Wrapper for the InputStream that ensures an entire top-level value is available. private final IonReaderLookaheadBuffer lookahead; // Buffer that stores top-level values. private final ResizingPipedInputStream buffer; // Converter between scalar types, allowing, for example, for a value encoded as an Ion float to be returned as a // Java `long` via `IonReader.longValue()`. private final _Private_ScalarConversions.ValueVariant scalarConverter; // Stack to hold container info. Stepping into a container results in a push; stepping out results in a pop. private final _Private_RecyclingStack containerStack; private final Utf8StringDecoder utf8Decoder = Utf8StringDecoderPool.getInstance().getOrCreate(); // The symbol IDs for the annotations on the current value. private final IntList annotationSids; // True if the annotation iterator will be reused across values; otherwise, false. private final boolean isAnnotationIteratorReuseEnabled; // Reusable iterator over the annotations on the current value. private final AnnotationIterator annotationIterator; // The text representations of the symbol table that is currently in scope, indexed by symbol ID. If the element at // a particular index is null, that symbol has unknown text. private final List symbols; // The catalog used by the reader to resolve shared symbol table imports. private final IonCatalog catalog; // The shared symbol tables imported by the local symbol table that is currently in scope. private LocalSymbolTableImports imports = ION_1_0_IMPORTS; // A map of symbol ID to SymbolToken representation. Because most use cases only require symbol text, this // is used only if necessary to avoid imposing the extra expense on all symbol lookups. private List symbolTokensById = null; // The cached SymbolTable representation of the current local symbol table. Invalidated whenever a local // symbol table is encountered in the stream. private SymbolTable cachedReadOnlySymbolTable = null; // The SymbolTable that was transferred via the last call to pop_passed_symbol_table. private SymbolTable symbolTableLastTransferred = null; // The symbol ID of the current value's field name, or -1 if the current value is not in a struct. private int fieldNameSid = -1; // The major version of the Ion encoding currently being read. private int majorVersion = 1; // The minor version of the Ion encoding currently being read. private int minorVersion = 0; // The number of bytes of a lob value that the user has consumed, allowing for piecewise reads. private int lobBytesRead = 0; // The type of value at which the reader is currently positioned. private IonType valueType = null; // Information about the type ID byte for the value at which the reader is currently positioned. private IonTypeID valueTypeID = null; // Indicates whether there are annotations on the current value. private boolean hasAnnotations = false; // Indicates whether a complete top-level value is currenty buffered. private boolean completeValueBuffered = false; // --- Byte position markers --- // Note: absolute positions/indexes can be used because the bytes that represent a single top-level value are // always handled in two sequential phases: first, the bytes are buffered, and then they are read. These operations // will never be interleaved during the processing of a single value. As a result, the underlying buffer // will always hold all of the bytes for a single top-level value in a contiguous sequence, even if the buffer // has to grow to hold all of the value's bytes. // The buffer position of the first byte of the value representation (after the type ID and optional length field). private int valueStartPosition = -1; // The buffer position of the byte after the last byte in the value representation. private int valueEndPosition = -1; // The buffer position of the first byte of the annotation wrapper for the current value. private int annotationStartPosition = -1; // The buffer position of the byte after the last byte in the annotation wrapper for the current value. private int annotationEndPosition = -1; // The index of the next byte to peek from the underlying buffer. private int peekIndex = -1; // ------ /** * Constructor. * @param builder the builder containing the configuration for the new reader. * @param inputStream the InputStream that provides binary Ion data. */ IonReaderBinaryIncremental(IonReaderBuilder builder, InputStream inputStream) { this.inputStream = inputStream; this.catalog = builder.getCatalog() == null ? EMPTY_CATALOG : builder.getCatalog(); if (builder.isAnnotationIteratorReuseEnabled()) { isAnnotationIteratorReuseEnabled = true; annotationIterator = new AnnotationIterator(); } else { isAnnotationIteratorReuseEnabled = false; annotationIterator = null; } IonBufferConfiguration configuration = builder.getBufferConfiguration(); if (configuration == null) { configuration = STANDARD_BUFFER_CONFIGURATION; if (inputStream instanceof ByteArrayInputStream) { // ByteArrayInputStreams are fixed-size streams. Clamp the reader's internal buffer size at the size of // the stream to avoid wastefully allocating extra space that will never be needed. It is still // preferable for the user to manually specify the buffer size if it's less than the default, as doing // so allows this branch to be skipped. int fixedBufferSize; try { fixedBufferSize = inputStream.available(); } catch (IOException e) { // ByteArrayInputStream.available() does not throw. throw new IllegalStateException(e); } if (configuration.getInitialBufferSize() > fixedBufferSize) { configuration = FIXED_SIZE_CONFIGURATIONS[logBase2(fixedBufferSize)]; } } } lookahead = new IonReaderLookaheadBuffer(configuration, inputStream); buffer = (ResizingPipedInputStream) lookahead.getPipe(); containerStack = new _Private_RecyclingStack( CONTAINER_STACK_INITIAL_CAPACITY, CONTAINER_INFO_FACTORY ); annotationSids = new IntList(ANNOTATIONS_LIST_INITIAL_CAPACITY); symbols = new ArrayList(SYMBOLS_LIST_INITIAL_CAPACITY); scalarConverter = new _Private_ScalarConversions.ValueVariant(); resetImports(); } /** * Reusable iterator over the annotations on the current value. */ private class AnnotationIterator implements Iterator { // The byte position of the annotation to return from the next call to next(). private int nextAnnotationPeekIndex; @Override public boolean hasNext() { return nextAnnotationPeekIndex < annotationEndPosition; } @Override public String next() { int savedPeekIndex = peekIndex; peekIndex = nextAnnotationPeekIndex; int sid = readVarUInt(); nextAnnotationPeekIndex = peekIndex; peekIndex = savedPeekIndex; String annotation = getSymbol(sid); if (annotation == null) { throw new UnknownSymbolException(sid); } return annotation; } @Override public void remove() { throw new UnsupportedOperationException("This iterator does not support element removal."); } /** * Prepare the iterator to iterate over the annotations on the current value. */ void ready() { nextAnnotationPeekIndex = annotationStartPosition; } /** * Invalidate the iterator so that all future calls to {@link #hasNext()} will return false until the * next call to {@link #ready()}. */ void invalidate() { nextAnnotationPeekIndex = Integer.MAX_VALUE; } } /** * Non-reusable iterator over the annotations on the current value. May be iterated even if the reader advances * past the current value. */ private class SingleUseAnnotationIterator implements Iterator { // All of the annotation SIDs on the current value. private final IntList annotationSids; // The index into `annotationSids` containing the next annotation to be returned. private int index = 0; SingleUseAnnotationIterator() { annotationSids = new IntList(getAnnotationSids()); } @Override public boolean hasNext() { return index < annotationSids.size(); } @Override public String next() { int sid = annotationSids.get(index); String annotation = getSymbol(sid); if (annotation == null) { throw new UnknownSymbolException(sid); } index++; return annotation; } @Override public void remove() { throw new UnsupportedOperationException("This iterator does not support element removal."); } } /** * SymbolToken implementation that includes ImportLocation. */ static class SymbolTokenImpl implements _Private_SymbolToken { // The symbol's text, or null if the text is unknown. private final String text; // The local symbol ID of this symbol within a particular local symbol table. private final int sid; // The import location of the symbol (only relevant if the text is unknown). private final ImportLocation importLocation; SymbolTokenImpl(String text, int sid, ImportLocation importLocation) { this.text = text; this.sid = sid; this.importLocation = importLocation; } @Override public String getText() { return text; } @Override public String assumeText() { if (text == null) { throw new UnknownSymbolException(sid); } return text; } @Override public int getSid() { return sid; } // Will be @Override once added to the SymbolToken interface. public ImportLocation getImportLocation() { return importLocation; } @Override public String toString() { return String.format("SymbolToken::{text: %s, sid: %d, importLocation: %s}", text, sid, importLocation); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof SymbolToken)) return false; // NOTE: once ImportLocation is available via the SymbolToken interface, it should be compared here // when text is null. SymbolToken other = (SymbolToken) o; if(getText() == null || other.getText() == null) { return getText() == other.getText(); } return getText().equals(other.getText()); } @Override public int hashCode() { if(getText() != null) return getText().hashCode(); return 0; } } /** * Gets the system symbol table for the Ion version currently active. * @return a system SymbolTable. */ private SymbolTable getSystemSymbolTable() { // Note: Ion 1.1 currently proposes changes to the system symbol table. If this is finalized, then // 'majorVersion' cannot be used to look up the system symbol table; both 'majorVersion' and 'minorVersion' // will need to be used. return SharedSymbolTable.getSystemSymbolTable(majorVersion); } /** * Read-only snapshot of the local symbol table at the reader's current position. */ private class LocalSymbolTableSnapshot implements SymbolTable, SymbolTableAsStruct { // The system symbol table. private final SymbolTable system = IonReaderBinaryIncremental.this.getSystemSymbolTable(); // The max ID of this local symbol table. private final int maxId; // The shared symbol tables imported by this local symbol table. private final LocalSymbolTableImports importedTables; // Map representation of this symbol table. Keys are symbol text; values are the lowest symbol ID that maps // to that text. final Map mapView; // List representation of this symbol table, indexed by symbol ID. final List listView; private SymbolTableStructCache structCache = null; LocalSymbolTableSnapshot() { int importsMaxId = imports.getMaxId(); int numberOfLocalSymbols = symbols.size(); // Note: 'imports' is immutable, so a clone is not needed. importedTables = imports; maxId = importsMaxId + numberOfLocalSymbols; // Map with initial size the number of symbols and load factor 1, meaning it must be full before growing. // It is not expected to grow. listView = new ArrayList(symbols.subList(0, numberOfLocalSymbols)); mapView = new HashMap((int) Math.ceil(numberOfLocalSymbols / 0.75), 0.75f); for (int i = 0; i < numberOfLocalSymbols; i++) { String symbol = listView.get(i); if (symbol != null) { mapView.put(symbol, i + importsMaxId + 1); } } } @Override public String getName() { return null; } @Override public int getVersion() { return 0; } @Override public boolean isLocalTable() { return true; } @Override public boolean isSharedTable() { return false; } @Override public boolean isSubstitute() { return false; } @Override public boolean isSystemTable() { return false; } @Override public SymbolTable getSystemSymbolTable() { return system; } @Override public String getIonVersionId() { return system.getIonVersionId(); } @Override public SymbolTable[] getImportedTables() { return importedTables.getImportedTables(); } @Override public int getImportedMaxId() { return importedTables.getMaxId(); } @Override public SymbolToken find(String text) { SymbolToken token = importedTables.find(text); if (token != null) { return token; } Integer sid = mapView.get(text); if (sid == null) { return null; } // The following per-call allocation is intentional. When weighed against the alternative of making // 'mapView' a 'Map` instead of a `Map`, the following points should // be considered: // 1. A LocalSymbolTableSnapshot is only created when getSymbolTable() is called on the reader. The reader // does not use the LocalSymbolTableSnapshot internally. There are two cases when getSymbolTable() would be // called: a) when the user calls it, which will basically never happen, and b) when the user uses // IonSystem.iterate over the reader, in which case each top-level value holds a reference to the symbol // table that was in scope when it occurred. In case a), in addition to rarely being called at all, it // would be even rarer for a user to use find() to retrieve each symbol (especially more than once) from the // returned symbol table. Case b) may be called more frequently, but it remains equally rare that a user // would retrieve each symbol at least once. // 2. If we make mapView a Map, then we are guaranteeing that we will allocate at least // one SymbolToken per symbol (because mapView is created in the constructor of LocalSymbolTableSnapshot) // even though it's unlikely most will ever be needed. return new SymbolTokenImpl(text, sid, null); } @Override public int findSymbol(String name) { Integer sid = importedTables.findSymbol(name); if (sid > UNKNOWN_SYMBOL_ID) { return sid; } sid = mapView.get(name); if (sid == null) { return UNKNOWN_SYMBOL_ID; } return sid; } @Override public String findKnownSymbol(int id) { if (id < 0) { throw new IllegalArgumentException("Symbol IDs must be at least 0."); } if (id > getMaxId()) { return null; } return IonReaderBinaryIncremental.this.getSymbolString(id, importedTables, listView); } @Override public Iterator iterateDeclaredSymbolNames() { return new Iterator() { private int index = 0; @Override public boolean hasNext() { return index < listView.size(); } @Override public String next() { String symbol = listView.get(index); index++; return symbol; } @Override public void remove() { throw new UnsupportedOperationException("This iterator does not support element removal."); } }; } @Override public SymbolToken intern(String text) { SymbolToken token = find(text); if (token != null) { return token; } throw new ReadOnlyValueException(); } @Override public int getMaxId() { return maxId; } @Override public boolean isReadOnly() { return true; } @Override public void makeReadOnly() { // The symbol table is already read-only. } @Override public void writeTo(IonWriter writer) throws IOException { IonReader reader = new SymbolTableReader(this); writer.writeValues(reader); } @Override public String toString() { return "(LocalSymbolTable max_id:" + getMaxId() + ')'; } @Override public IonStruct getIonRepresentation(ValueFactory valueFactory) { if (structCache == null) { structCache = new SymbolTableStructCache(this, getImportedTables(), null); } return structCache.getIonRepresentation(valueFactory); } } /** * Throw if the reader is attempting to process an Ion version that it does not support. */ private void requireSupportedIonVersion() { if (majorVersion != 1 || minorVersion != 0) { throw new IonException(String.format("Unsupported Ion version: %d.%d", majorVersion, minorVersion)); } } /** * Reset the local symbol table to the system symbol table. */ private void resetSymbolTable() { // Note: when there is a new version of Ion, check majorVersion and minorVersion here and set the appropriate // system symbol table. symbols.clear(); cachedReadOnlySymbolTable = null; if (symbolTokensById != null) { symbolTokensById.clear(); } } /** * Resets the value's annotations. */ private void resetAnnotations() { hasAnnotations = false; if (isAnnotationIteratorReuseEnabled) { annotationIterator.invalidate(); } } /** * Clear the list of imported shared symbol tables. */ private void resetImports() { // Note: when support for the next version of Ion is added, conditionals on 'majorVersion' and 'minorVersion' // must be added here. imports = ION_1_0_IMPORTS; } /** * Creates a shared symbol table import, resolving it from the catalog if possible. * @param name the name of the shared symbol table. * @param version the version of the shared symbol table. * @param maxId the max_id of the shared symbol table. This value takes precedence over the actual max_id for the * shared symbol table at the requested version. */ private SymbolTable createImport(String name, int version, int maxId) { SymbolTable shared = catalog.getTable(name, version); if (shared == null) { // No match. All symbol IDs that fall within this shared symbol table's range will have unknown text. return new SubstituteSymbolTable(name, version, maxId); } else if (shared.getMaxId() != maxId || shared.getVersion() != version) { // Partial match. If the requested max_id exceeds the actual max_id of the resolved shared symbol table, // symbol IDs that exceed the max_id of the resolved shared symbol table will have unknown text. return new SubstituteSymbolTable(shared, version, maxId); } else { // Exact match; the resolved shared symbol table may be used as-is. return shared; } } /** * Gets the String representation of the given symbol ID. It is the caller's responsibility to ensure that the * given symbol ID is within the max ID of the symbol table. * @param sid the symbol ID. * @param importedSymbols the symbol table's shared symbol table imports. * @param localSymbols the symbol table's local symbols. * @return a String, which will be null if the requested symbol ID has undefined text. */ private String getSymbolString(int sid, LocalSymbolTableImports importedSymbols, List localSymbols) { if (sid <= importedSymbols.getMaxId()) { return importedSymbols.findKnownSymbol(sid); } return localSymbols.get(sid - (importedSymbols.getMaxId() + 1)); } /** * Calculates the symbol table's max ID. * @return the max ID. */ private int maxSymbolId() { return symbols.size() + imports.getMaxId(); } /** * Retrieves the String text for the given symbol ID. * @param sid a symbol ID. * @return a String. */ private String getSymbol(int sid) { if (sid > maxSymbolId()) { throw new IonException("Symbol ID exceeds the max ID of the symbol table."); } return getSymbolString(sid, imports, symbols); } /** * Creates a SymbolToken representation of the given symbol ID. * @param sid a symbol ID. * @return a SymbolToken. */ private SymbolToken getSymbolToken(int sid) { int symbolTableSize = maxSymbolId() + 1; if (symbolTokensById == null) { symbolTokensById = new ArrayList(symbolTableSize); } if (symbolTokensById.size() < symbolTableSize) { for (int i = symbolTokensById.size(); i < symbolTableSize; i++) { symbolTokensById.add(null); } } if (sid >= symbolTableSize) { throw new IonException("Symbol ID exceeds the max ID of the symbol table."); } SymbolToken token = symbolTokensById.get(sid); if (token == null) { String text = getSymbolString(sid, imports, symbols); ImportLocation importLocation = null; if (text == null) { // Note: this will never be a system symbol. if (sid > 0 && sid <= imports.getMaxId()) { importLocation = imports.getImportLocation(sid); } else { // All symbols with unknown text in the local symbol range are equivalent to symbol zero. sid = 0; } } token = new SymbolTokenImpl(text, sid, importLocation); symbolTokensById.set(sid, token); } return token; } /** * Reads a local symbol table from the buffer. * @param marker marker for the start and end positions of the local symbol table in the buffer. */ private void readSymbolTable(IonReaderLookaheadBuffer.Marker marker) { peekIndex = marker.startIndex; boolean isAppend = false; boolean hasSeenImports = false; boolean hasSeenSymbols = false; int symbolsPosition = -1; int symbolsEndPosition = -1; List newImports; while (peekIndex < marker.endIndex) { fieldNameSid = readVarUInt(); IonTypeID typeID = readTypeId(); calculateEndPosition(typeID); int currentValueEndPosition = valueEndPosition; if (fieldNameSid == SystemSymbolIDs.IMPORTS_ID) { if (hasSeenImports) { throw new IonException("Symbol table contained multiple imports fields."); } if (typeID.type == IonType.SYMBOL) { isAppend = readUInt(peekIndex, currentValueEndPosition) == SystemSymbolIDs.ION_SYMBOL_TABLE_ID; peekIndex = currentValueEndPosition; } else if (typeID.type == IonType.LIST) { resetImports(); newImports = new ArrayList(3); newImports.add(getSystemSymbolTable()); stepIn(); IonType type = next(); while (type != null) { String name = null; int version = -1; int maxId = -1; if (type == IonType.STRUCT) { stepIn(); type = next(); while (type != null) { int fieldSid = getFieldId(); if (fieldSid == SystemSymbolIDs.NAME_ID) { if (type == IonType.STRING) { name = stringValue(); } } else if (fieldSid == SystemSymbolIDs.VERSION_ID) { if (type == IonType.INT) { version = intValue(); } } else if (fieldSid == SystemSymbolIDs.MAX_ID_ID) { if (type == IonType.INT) { maxId = intValue(); } } type = next(); } stepOut(); } newImports.add(createImport(name, version, maxId)); type = next(); } stepOut(); imports = new LocalSymbolTableImports(newImports); } if (!isAppend) { // Clear the existing symbols before adding the new imported symbols. resetSymbolTable(); } hasSeenImports = true; } else if (fieldNameSid == SystemSymbolIDs.SYMBOLS_ID) { if (hasSeenSymbols) { throw new IonException("Symbol table contained multiple symbols fields."); } if (typeID.type == IonType.LIST) { // Just record this position and skip forward. Come back after the imports (if any) are parsed. symbolsPosition = peekIndex; symbolsEndPosition = currentValueEndPosition; } hasSeenSymbols = true; } peekIndex = currentValueEndPosition; } if (peekIndex > marker.endIndex) { throw new IonException("Malformed symbol table. Child values exceeded the length declared in the header."); } if (!hasSeenImports) { resetSymbolTable(); resetImports(); } if (symbolsPosition > -1) { peekIndex = symbolsPosition; valueType = IonType.LIST; valueEndPosition = symbolsEndPosition; stepIn(); while (next() != null) { if (valueType != IonType.STRING) { symbols.add(null); } else { symbols.add(stringValue()); } } stepOut(); peekIndex = valueEndPosition; } } /** * Advance the reader to the next top-level value. Buffers an entire top-level value, reads any IVMs and/or local * symbol tables that precede the value, and sets the byte positions of important components of the value. */ private void nextAtTopLevel() { if (completeValueBuffered) { // There is already data buffered, but the user is choosing to skip it. buffer.seekTo(valueEndPosition); completeValueBuffered = false; } try { lookahead.fillInput(); } catch (Exception e) { throw new IonException(e); } if (lookahead.moreDataRequired()) { valueType = null; valueTypeID = null; return; } completeValueBuffered = true; if (lookahead.getIvmIndex() > -1) { peekIndex = lookahead.getIvmIndex(); majorVersion = buffer.peek(peekIndex++); minorVersion = buffer.peek(peekIndex++); if (buffer.peek(peekIndex++) != IVM_FINAL_BYTE) { throw new IonException("Invalid Ion version marker."); } requireSupportedIonVersion(); resetSymbolTable(); resetImports(); lookahead.resetIvmIndex(); } else if (peekIndex < 0) { // peekIndex is initialized to -1 and only increases. This branch is reached if the IVM does not occur // first in the stream. This is necessary because currently a binary incremental reader will be created if // an empty stream is provided to the IonReaderBuilder. If, once bytes appear in the stream, those bytes do // not represent valid binary Ion, a quick failure is necessary. throw new IonException("Binary Ion must start with an Ion version marker."); } List symbolTableMarkers = lookahead.getSymbolTableMarkers(); if (!symbolTableMarkers.isEmpty()) { // The cached SymbolTable (if any) is a snapshot in time, so it must be cleared whenever a new symbol // table is read regardless of whether the new LST is an append or a reset. cachedReadOnlySymbolTable = null; for (IonReaderLookaheadBuffer.Marker symbolTableMarker : symbolTableMarkers) { readSymbolTable(symbolTableMarker); } lookahead.resetSymbolTableMarkers(); } peekIndex = lookahead.getValueStart(); hasAnnotations = lookahead.hasAnnotations(); if (hasAnnotations) { if (peekIndex >= lookahead.getValueEnd()) { throw new IonException("Annotation wrappers without values are invalid."); } annotationSids.clear(); IonReaderLookaheadBuffer.Marker annotationSidsMarker = lookahead.getAnnotationSidsMarker(); annotationStartPosition = annotationSidsMarker.startIndex; annotationEndPosition = annotationSidsMarker.endIndex; peekIndex = annotationEndPosition; valueTypeID = IonTypeID.TYPE_IDS[buffer.peek(peekIndex++)]; int wrappedValueLength = valueTypeID.length; if (valueTypeID.variableLength) { wrappedValueLength = readVarUInt(); } valueType = valueTypeID.type; if (valueType == IonTypeID.ION_TYPE_ANNOTATION_WRAPPER) { throw new IonException("Nested annotations are invalid."); } if (peekIndex + wrappedValueLength != lookahead.getValueEnd()) { throw new IonException("Mismatched annotation wrapper length."); } } else { valueTypeID = lookahead.getValueTid(); valueType = valueTypeID.type; } valueStartPosition = peekIndex; valueEndPosition = lookahead.getValueEnd(); lookahead.resetNopPadIndex(); } /** * Reads the type ID byte. * @return the TypeAndLength descriptor for the type ID byte. */ private IonTypeID readTypeId() { valueTypeID = IonTypeID.TYPE_IDS[buffer.peek(peekIndex++)]; if (!valueTypeID.isValid) { throw new IonException("Invalid type ID."); } valueType = valueTypeID.type; return valueTypeID; } /** * Calculates the end position for the given type ID descriptor. * @param typeID the type ID descriptor. */ private void calculateEndPosition(IonTypeID typeID) { if (typeID.variableLength) { valueEndPosition = readVarUInt() + peekIndex; } else { valueEndPosition = typeID.length + peekIndex; } } @Override public boolean hasNext() { throw new UnsupportedOperationException("Not implemented"); } /** * Marks the end of the current container by indicating that the reader is no longer positioned on a value. */ private void endContainer() { valueType = null; valueTypeID = null; annotationStartPosition = -1; annotationEndPosition = -1; hasAnnotations = false; } /** * Advance the reader to the next value within a container, which must already be buffered. */ private void nextBelowTopLevel() { // Seek past the previous value. if (peekIndex < valueEndPosition) { peekIndex = valueEndPosition; } if (peekIndex >= containerStack.peek().endPosition) { endContainer(); } else { if (containerStack.peek().type == IonType.STRUCT) { fieldNameSid = readVarUInt(); } IonTypeID typeID = readTypeId(); while (typeID.isNopPad) { calculateEndPosition(typeID); peekIndex = valueEndPosition; if (peekIndex >= containerStack.peek().endPosition) { endContainer(); return; } if (containerStack.peek().type == IonType.STRUCT) { fieldNameSid = readVarUInt(); } typeID = readTypeId(); } calculateEndPosition(typeID); if (valueType == IonTypeID.ION_TYPE_ANNOTATION_WRAPPER) { hasAnnotations = true; annotationSids.clear(); int annotationsLength = readVarUInt(); annotationStartPosition = peekIndex; annotationEndPosition = annotationStartPosition + annotationsLength; peekIndex = annotationEndPosition; typeID = readTypeId(); if (typeID.isNopPad) { throw new IonException( "Invalid annotation wrapper: NOP pad may not occur inside an annotation wrapper." ); } if (valueType == IonTypeID.ION_TYPE_ANNOTATION_WRAPPER) { throw new IonException("Nested annotations are invalid."); } long annotationWrapperEndPosition = valueEndPosition; calculateEndPosition(typeID); if (annotationWrapperEndPosition != valueEndPosition) { throw new IonException( "Invalid annotation wrapper: end of the wrapper did not match end of the value." ); } } else { annotationStartPosition = -1; annotationEndPosition = -1; hasAnnotations = false; if (valueEndPosition > containerStack.peek().endPosition) { throw new IonException("Value overflowed its container."); } } if (!valueTypeID.isValid) { throw new IonException("Invalid type ID."); } valueStartPosition = peekIndex; } } @Override public IonType next() { fieldNameSid = -1; lobBytesRead = 0; valueStartPosition = -1; resetAnnotations(); if (containerStack.isEmpty()) { nextAtTopLevel(); } else { nextBelowTopLevel(); } // Note: the following check is necessary to catch empty ordered structs, which are prohibited by the spec. // Unfortunately, this requires a check on every value for a condition that will probably never happen. if ( valueType == IonType.STRUCT && valueTypeID.lowerNibble == IonTypeID.ORDERED_STRUCT_NIBBLE && valueStartPosition == valueEndPosition ) { throw new IonException("Ordered struct must not be empty."); } return valueType; } @Override public void stepIn() { if (!IonType.isContainer(valueType)) { throw new IonException("Must be positioned on a container to step in."); } // Note: the IonReader interface dictates that stepping into a null container has the same behavior as // an empty container. ContainerInfo containerInfo = containerStack.push(); containerInfo.type = valueType; containerInfo.endPosition = valueEndPosition; valueType = null; valueTypeID = null; valueEndPosition = -1; fieldNameSid = -1; valueStartPosition = -1; } @Override public void stepOut() { if (containerStack.isEmpty()) { // Note: this is IllegalStateException for consistency with the other binary IonReader implementation. throw new IllegalStateException("Cannot step out at top level."); } ContainerInfo containerInfo = containerStack.pop(); valueEndPosition = containerInfo.endPosition; valueType = null; valueTypeID = null; fieldNameSid = -1; valueStartPosition = -1; } @Override public int getDepth() { return containerStack.size(); } @Override public SymbolTable getSymbolTable() { if (cachedReadOnlySymbolTable == null) { if (symbols.size() == 0 && imports == ION_1_0_IMPORTS) { cachedReadOnlySymbolTable = imports.getSystemSymbolTable(); } else { cachedReadOnlySymbolTable = new LocalSymbolTableSnapshot(); } } return cachedReadOnlySymbolTable; } @Override public SymbolTable pop_passed_symbol_table() { SymbolTable currentSymbolTable = getSymbolTable(); if (currentSymbolTable == symbolTableLastTransferred) { // This symbol table has already been returned. Since the contract is that it is a "pop", it should not // be returned twice. return null; } symbolTableLastTransferred = currentSymbolTable; return symbolTableLastTransferred; } @Override public IonType getType() { return valueType; } @Override public IntegerSize getIntegerSize() { if (valueType != IonType.INT || isNullValue()) { return null; } if (valueTypeID.length < INT_SIZE_IN_BYTES) { // Note: this is conservative. Most integers of size 4 also fit in an int, but since exactly the // same parsing code is used for ints and longs, there is no point wasting the time to determine the // smallest possible type. return IntegerSize.INT; } else if (valueTypeID.length < LONG_SIZE_IN_BYTES) { return IntegerSize.LONG; } else if (valueTypeID.length == LONG_SIZE_IN_BYTES) { // Because creating BigIntegers is so expensive, it is worth it to look ahead and determine exactly // which 8-byte integers can fit in a long. if (valueTypeID.isNegativeInt) { // The smallest negative 8-byte integer that can fit in a long is -0x80_00_00_00_00_00_00_00. int firstByte = buffer.peek(valueStartPosition); if (firstByte < MOST_SIGNIFICANT_BYTE_OF_MIN_LONG) { return IntegerSize.LONG; } else if (firstByte > MOST_SIGNIFICANT_BYTE_OF_MIN_LONG) { return IntegerSize.BIG_INTEGER; } for (int i = valueStartPosition + 1; i < valueEndPosition; i++) { if (0x00 != buffer.peek(i)) { return IntegerSize.BIG_INTEGER; } } } else { // The largest positive 8-byte integer that can fit in a long is 0x7F_FF_FF_FF_FF_FF_FF_FF. if (buffer.peek(valueStartPosition) > MOST_SIGNIFICANT_BYTE_OF_MAX_LONG) { return IntegerSize.BIG_INTEGER; } } return IntegerSize.LONG; } return IntegerSize.BIG_INTEGER; } /** * Require that the given type matches the type of the current value. * @param required the required type of current value. */ private void requireType(IonType required) { if (required != valueType) { // Note: this is IllegalStateException to match the behavior of the other binary IonReader implementation. throw new IllegalStateException( String.format("Invalid type. Required %s but found %s.", required, valueType) ); } } /** * Reads a VarUInt. * @return the value. */ private int readVarUInt() { int currentByte = 0; int result = 0; while ((currentByte & HIGHEST_BIT_BITMASK) == 0) { currentByte = buffer.peek(peekIndex++); result = (result << VALUE_BITS_PER_VARUINT_BYTE) | (currentByte & LOWER_SEVEN_BITS_BITMASK); } return result; } /** * Reads a UInt. * @param limit the position of the first byte after the end of the UInt value. * @return the value. */ private long readUInt(int startIndex, int limit) { long result = 0; for (int i = startIndex; i < limit; i++) { result = (result << VALUE_BITS_PER_UINT_BYTE) | buffer.peek(i); } return result; } /** * Reads a UInt starting at `valueStartPosition` and ending at `valueEndPosition`. * @return the value. */ private long readUInt() { return readUInt(valueStartPosition, valueEndPosition); } /** * Reads a VarInt. * @param firstByte the first byte of the VarInt representation, which has already been retrieved from the buffer. * @return the value. */ private int readVarInt(int firstByte) { int currentByte = firstByte; int sign = (currentByte & VAR_INT_SIGN_BITMASK) == 0 ? 1 : -1; int result = currentByte & LOWER_SIX_BITS_BITMASK; while ((currentByte & HIGHEST_BIT_BITMASK) == 0) { currentByte = buffer.peek(peekIndex++); result = (result << VALUE_BITS_PER_VARUINT_BYTE) | (currentByte & LOWER_SEVEN_BITS_BITMASK); } return result * sign; } /** * Reads a VarInt. * @return the value. */ private int readVarInt() { return readVarInt(buffer.peek(peekIndex++)); } // Scratch space for various byte sizes. Only for use while computing a single value. private final byte[][] scratchForSize = new byte[][] { new byte[0], new byte[1], new byte[2], new byte[3], new byte[4], new byte[5], new byte[6], new byte[7], new byte[8], new byte[9], new byte[10], new byte[11], new byte[12], }; /** * Copy the requested number of bytes from the buffer into a scratch buffer of exactly the requested length. * @param startIndex the start index from which to copy. * @param length the number of bytes to copy. * @return the scratch byte array. */ private byte[] copyBytesToScratch(int startIndex, int length) { // Note: using reusable scratch buffers makes reading ints and decimals 1-5% faster and causes much less // GC churn. byte[] bytes = null; if (length < scratchForSize.length) { bytes = scratchForSize[length]; } if (bytes == null) { bytes = new byte[length]; } // The correct number of bytes will be requested from the buffer, so the limit is set at the capacity to // avoid having to calculate a limit. buffer.copyBytes(startIndex, bytes, 0, bytes.length); return bytes; } /** * Reads a UInt value into a BigInteger. * @param isNegative true if the resulting BigInteger value should be negative; false if it should be positive. * @return the value. */ private BigInteger readUIntAsBigInteger(boolean isNegative) { int length = valueEndPosition - valueStartPosition; // NOTE: unfortunately, there is no BigInteger(int signum, byte[] bits, int offset, int length) constructor // until JDK 9, so copying to scratch space is always required. Migrating to the new constructor will // lead to a significant performance improvement. byte[] magnitude = copyBytesToScratch(valueStartPosition, length); int signum = isNegative ? -1 : 1; return new BigInteger(signum, magnitude); } /** * Get and clear the most significant bit in the given byte array. * @param intBytes bytes representing a signed int. * @return -1 if the most significant bit was set; otherwise, 1. */ private int getAndClearSignBit(byte[] intBytes) { boolean isNegative = (intBytes[0] & HIGHEST_BIT_BITMASK) != 0; int signum = isNegative ? -1 : 1; if (isNegative) { intBytes[0] &= LOWER_SEVEN_BITS_BITMASK; } return signum; } /** * Reads an Int value into a BigInteger. * @param limit the position of the first byte after the end of the UInt value. * @return the value. */ private BigInteger readIntAsBigInteger(int limit) { BigInteger value; int length = limit - peekIndex; if (length > 0) { // NOTE: unfortunately, there is no BigInteger(int signum, byte[] bits, int offset, int length) constructor // until JDK 9, so copying to scratch space is always required. Migrating to the new constructor will // lead to a significant performance improvement. byte[] bytes = copyBytesToScratch(peekIndex, length); value = new BigInteger(getAndClearSignBit(bytes), bytes); } else { value = BigInteger.ZERO; } return value; } @Override public long longValue() { long value; if (valueType == IonType.INT) { if (valueTypeID.length == 0) { return 0; } value = readUInt(); if (valueTypeID.isNegativeInt) { if (value == 0) { throw new IonException("Int zero may not be negative."); } value *= -1; } } else if (valueType == IonType.FLOAT) { scalarConverter.addValue(doubleValue()); scalarConverter.setAuthoritativeType(_Private_ScalarConversions.AS_TYPE.double_value); scalarConverter.cast(scalarConverter.get_conversion_fnid(_Private_ScalarConversions.AS_TYPE.long_value)); value = scalarConverter.getLong(); scalarConverter.clear(); } else if (valueType == IonType.DECIMAL) { scalarConverter.addValue(decimalValue()); scalarConverter.setAuthoritativeType(_Private_ScalarConversions.AS_TYPE.decimal_value); scalarConverter.cast(scalarConverter.get_conversion_fnid(_Private_ScalarConversions.AS_TYPE.long_value)); value = scalarConverter.getLong(); scalarConverter.clear(); } else { throw new IllegalStateException("longValue() may only be called on values of type int, float, or decimal."); } return value; } @Override public BigInteger bigIntegerValue() { BigInteger value; if (valueType == IonType.INT) { if (isNullValue()) { // NOTE: this mimics existing behavior, but should probably be undefined (as, e.g., longValue() is in this // case). return null; } if (valueTypeID.length == 0) { return BigInteger.ZERO; } value = readUIntAsBigInteger(valueTypeID.isNegativeInt); if (valueTypeID.isNegativeInt && value.signum() == 0) { throw new IonException("Int zero may not be negative."); } } else if (valueType == IonType.FLOAT) { if (isNullValue()) { value = null; } else { scalarConverter.addValue(doubleValue()); scalarConverter.setAuthoritativeType(_Private_ScalarConversions.AS_TYPE.double_value); scalarConverter.cast(scalarConverter.get_conversion_fnid(_Private_ScalarConversions.AS_TYPE.bigInteger_value)); value = scalarConverter.getBigInteger(); scalarConverter.clear(); } } else { throw new IllegalStateException("bigIntegerValue() may only be called on values of type int or float."); } return value; } @Override public Date dateValue() { Timestamp timestamp = timestampValue(); if (timestamp == null) { return null; } return timestamp.dateValue(); } @Override public int intValue() { return (int) longValue(); } @Override public double doubleValue() { double value; if (valueType == IonType.FLOAT) { int length = valueEndPosition - valueStartPosition; if (length == 0) { return 0.0d; } ByteBuffer bytes = buffer.getByteBuffer(valueStartPosition, valueEndPosition); if (length == FLOAT_32_BYTE_LENGTH) { value = bytes.getFloat(); } else { // Note: there is no need to check for other lengths here; the type ID byte is validated during next(). value = bytes.getDouble(); } } else if (valueType == IonType.DECIMAL) { scalarConverter.addValue(decimalValue()); scalarConverter.setAuthoritativeType(_Private_ScalarConversions.AS_TYPE.decimal_value); scalarConverter.cast(scalarConverter.get_conversion_fnid(_Private_ScalarConversions.AS_TYPE.double_value)); value = scalarConverter.getDouble(); scalarConverter.clear(); } else { throw new IllegalStateException("doubleValue() may only be called on values of type float or decimal."); } return value; } /** * Decodes a string from the buffer into a String value. * @param valueStart the position in the buffer of the first byte in the string. * @param valueEnd the position in the buffer of the last byte in the string. * @return the value. */ private String readString(int valueStart, int valueEnd) { ByteBuffer utf8InputBuffer = buffer.getByteBuffer(valueStart, valueEnd); int numberOfBytes = valueEnd - valueStart; return utf8Decoder.decode(utf8InputBuffer, numberOfBytes); } @Override public String stringValue() { String value; if (valueType == IonType.STRING) { if (isNullValue()) { return null; } value = readString(valueStartPosition, valueEndPosition); } else if (valueType == IonType.SYMBOL) { if (isNullValue()) { return null; } int sid = (int) readUInt(); value = getSymbol(sid); if (value == null) { throw new UnknownSymbolException(sid); } } else { throw new IllegalStateException("Invalid type requested."); } return value; } @Override public SymbolToken symbolValue() { requireType(IonType.SYMBOL); if (isNullValue()) { return null; } int sid = (int) readUInt(); return getSymbolToken(sid); } @Override public int byteSize() { if (!IonType.isLob(valueType) && !isNullValue()) { throw new IonException("Reader must be positioned on a blob or clob."); } return valueEndPosition - valueStartPosition; } @Override public byte[] newBytes() { byte[] bytes = new byte[byteSize()]; // The correct number of bytes will be requested from the buffer, so the limit is set at the capacity to // avoid having to calculate a limit. buffer.copyBytes(valueStartPosition, bytes, 0, bytes.length); return bytes; } @Override public int getBytes(byte[] bytes, int offset, int len) { int length = Math.min(len, byteSize() - lobBytesRead); // The correct number of bytes will be requested from the buffer, so the limit is set at the capacity to // avoid having to calculate a limit. buffer.copyBytes(valueStartPosition + lobBytesRead, bytes, offset, length); lobBytesRead += length; return length; } /** * Reads a decimal value as a BigDecimal. * @return the value. */ private BigDecimal readBigDecimal() { int length = valueEndPosition - peekIndex; if (length == 0) { return BigDecimal.ZERO; } int scale = -readVarInt(); BigDecimal value; if (length < LONG_SIZE_IN_BYTES) { // No need to allocate a BigInteger to hold the coefficient. long coefficient = 0; int sign = 1; if (peekIndex < valueEndPosition) { int firstByte = buffer.peek(peekIndex++); sign = (firstByte & HIGHEST_BIT_BITMASK) == 0 ? 1 : -1; coefficient = firstByte & LOWER_SEVEN_BITS_BITMASK; } while (peekIndex < valueEndPosition) { coefficient = (coefficient << VALUE_BITS_PER_UINT_BYTE) | buffer.peek(peekIndex++); } value = BigDecimal.valueOf(coefficient * sign, scale); } else { // The coefficient may overflow a long, so a BigInteger is required. value = new BigDecimal(readIntAsBigInteger(valueEndPosition), scale); } return value; } /** * Reads a decimal value as a Decimal. * @return the value. */ private Decimal readDecimal() { int length = valueEndPosition - peekIndex; if (length == 0) { return Decimal.ZERO; } int scale = -readVarInt(); BigInteger coefficient; length = valueEndPosition - peekIndex; if (length > 0) { // NOTE: unfortunately, there is no BigInteger(int signum, byte[] bits, int offset, int length) constructor, // so copying to scratch space is always required. byte[] bits = copyBytesToScratch(peekIndex, length); int signum = getAndClearSignBit(bits); // NOTE: there is a BigInteger.valueOf(long unscaledValue, int scale) factory method that avoids allocating // a BigInteger for coefficients that fit in a long. See its use in readBigDecimal() above. Unfortunately, // it is not possible to use this for Decimal because the necessary BigDecimal constructor is // package-private. If a compatible BigDecimal constructor is added in a future JDK revision, a // corresponding factory method should be added to Decimal to enable this optimization. coefficient = new BigInteger(signum, bits); if (coefficient.signum() == 0 && signum < 0) { return Decimal.negativeZero(scale); } } else { coefficient = BigInteger.ZERO; } return Decimal.valueOf(coefficient, scale); } @Override public BigDecimal bigDecimalValue() { requireType(IonType.DECIMAL); if (isNullValue()) { return null; } peekIndex = valueStartPosition; return readBigDecimal(); } @Override public Decimal decimalValue() { requireType(IonType.DECIMAL); if (isNullValue()) { return null; } peekIndex = valueStartPosition; return readDecimal(); } @Override public Timestamp timestampValue() { requireType(IonType.TIMESTAMP); if (isNullValue()) { return null; } peekIndex = valueStartPosition; int firstByte = buffer.peek(peekIndex++); Integer offset = null; if (firstByte != VAR_INT_NEGATIVE_ZERO) { offset = readVarInt(firstByte); } int year = readVarUInt(); int month = 0; int day = 0; int hour = 0; int minute = 0; int second = 0; BigDecimal fractionalSecond = null; Timestamp.Precision precision = Timestamp.Precision.YEAR; if (peekIndex < valueEndPosition) { month = readVarUInt(); precision = Timestamp.Precision.MONTH; if (peekIndex < valueEndPosition) { day = readVarUInt(); precision = Timestamp.Precision.DAY; if (peekIndex < valueEndPosition) { hour = readVarUInt(); if (peekIndex >= valueEndPosition) { throw new IonException("Timestamps may not specify hour without specifying minute."); } minute = readVarUInt(); precision = Timestamp.Precision.MINUTE; if (peekIndex < valueEndPosition) { second = readVarUInt(); precision = Timestamp.Precision.SECOND; if (peekIndex < valueEndPosition) { fractionalSecond = readBigDecimal(); if (fractionalSecond.signum() < 0 || fractionalSecond.compareTo(BigDecimal.ONE) >= 0) { throw new IonException("The fractional seconds value in a timestamp must be greater" + "than or equal to zero and less than one."); } } } } } } try { return Timestamp.createFromUtcFields( precision, year, month, day, hour, minute, second, fractionalSecond, offset ); } catch (IllegalArgumentException e) { throw new IonException("Illegal timestamp encoding. ", e); } } /** * Gets the annotation symbol IDs for the current value, reading them from the buffer first if necessary. * @return the annotation symbol IDs, or an empty list if the current value is not annotated. */ private IntList getAnnotationSids() { if (annotationSids.isEmpty()) { int savedPeekIndex = peekIndex; peekIndex = annotationStartPosition; while (peekIndex < annotationEndPosition) { annotationSids.add(readVarUInt()); } peekIndex = savedPeekIndex; } return annotationSids; } @Override public String[] getTypeAnnotations() { if (hasAnnotations) { IntList annotationSids = getAnnotationSids(); String[] annotationArray = new String[annotationSids.size()]; for (int i = 0; i < annotationArray.length; i++) { String symbol = getSymbol(annotationSids.get(i)); if (symbol == null) { throw new UnknownSymbolException(annotationSids.get(i)); } annotationArray[i] = symbol; } return annotationArray; } return _Private_Utils.EMPTY_STRING_ARRAY; } @Override public SymbolToken[] getTypeAnnotationSymbols() { if (hasAnnotations) { IntList annotationSids = getAnnotationSids(); SymbolToken[] annotationArray = new SymbolToken[annotationSids.size()]; for (int i = 0; i < annotationArray.length; i++) { annotationArray[i] = getSymbolToken(annotationSids.get(i)); } return annotationArray; } return SymbolToken.EMPTY_ARRAY; } private static final Iterator EMPTY_ITERATOR = new Iterator() { @Override public boolean hasNext() { return false; } @Override public String next() { return null; } @Override public void remove() { throw new UnsupportedOperationException("Cannot remove from an empty iterator."); } }; @Override public Iterator iterateTypeAnnotations() { if (hasAnnotations) { if (isAnnotationIteratorReuseEnabled) { annotationIterator.ready(); return annotationIterator; } else { return new SingleUseAnnotationIterator(); } } return EMPTY_ITERATOR; } @Override public int getFieldId() { return fieldNameSid; } @Override public String getFieldName() { if (fieldNameSid < 0) { return null; } String fieldName = getSymbol(fieldNameSid); if (fieldName == null) { throw new UnknownSymbolException(fieldNameSid); } return fieldName; } @Override public SymbolToken getFieldNameSymbol() { if (fieldNameSid < 0) { return null; } return getSymbolToken(fieldNameSid); } @Override public boolean isNullValue() { return valueTypeID != null && valueTypeID.isNull; } @Override public boolean isInStruct() { return !containerStack.isEmpty() && containerStack.peek().type == IonType.STRUCT; } @Override public boolean booleanValue() { requireType(IonType.BOOL); return valueTypeID.lowerNibble == 1; } @Override public T asFacet(Class facetType) { return null; } @Override public void requireCompleteValue() { // NOTE: If we want to replace the other binary IonReader implementation with this one, the following // validation could be performed in next() if incremental mode is not enabled. That would allow this // implementation to behave in the same way as the other implementation when an incomplete value is // encountered. if (lookahead.isSkippingCurrentValue()) { throw new IonException("Unexpected EOF."); } if (lookahead.available() > 0 && lookahead.moreDataRequired()) { if (lookahead.getIvmIndex() < 0 || lookahead.available() != _Private_IonConstants.BINARY_VERSION_MARKER_SIZE) { throw new IonException("Unexpected EOF."); } } } @Override public void close() throws IOException { requireCompleteValue(); inputStream.close(); utf8Decoder.close(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy