Skip to content

Commit

Permalink
Adds IonRawTextWriter_1_1 (#682)
Browse files Browse the repository at this point in the history
  • Loading branch information
popematt authored and tgregg committed Jan 22, 2024
1 parent 908a52b commit 82d4497
Show file tree
Hide file tree
Showing 5 changed files with 1,437 additions and 4 deletions.
375 changes: 375 additions & 0 deletions src/main/java/com/amazon/ion/impl/IonRawTextWriter_1_1.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,375 @@
package com.amazon.ion.impl

import com.amazon.ion.*
import com.amazon.ion.impl.IonRawTextWriter_1_1.ContainerType.*
import com.amazon.ion.util.*
import java.math.BigDecimal
import java.math.BigInteger
import java.time.Instant

/**
* A raw writer for Ion 1.1 text. This should be combined with managed writer to handle concerns such as macros and
* possible symbol interning.
*
* Notes:
* - Never writes using "long string" syntax in order to simplify the writer.
* - Uses `[: ... ]` for expression groups.
* - Does not try to resolve symbol tokens. That is the concern of the managed writer.
*/
class IonRawTextWriter_1_1 internal constructor(
private val options: _Private_IonTextWriterBuilder,
private val output: _Private_IonTextAppender,
) : IonRawWriter_1_1 {

companion object {
const val IVM = "\$ion_1_1"
}

enum class ContainerType {
List,
SExp,
Struct,
Macro,
ExpressionGroup,
Top,
}

private var closed = false

private val ancestorContainersStack: ArrayList<ContainerType> = ArrayList()
private var currentContainer: ContainerType = Top
private var currentContainerHasValues = false

private var isPendingSeparator = false
private var isPendingLeadingWhitespace = false

private var fieldNameText: CharSequence? = null
private var fieldNameId: Int = -1
private var hasFieldName = false

private var annotationsTextBuffer = arrayOfNulls<CharSequence>(8)
private var annotationsIdBuffer = IntArray(8)
private var numAnnotations = 0

private inline fun openValue(valueWriterExpression: () -> Unit) {
if (currentContainer == Struct) {
confirm(hasFieldName) { "Values in a struct require a field name." }
}
val separatorCharacter = when (currentContainer) {
List, Struct -> ","
Macro, SExp -> " "
Top -> options.topLevelSeparator()
ExpressionGroup -> ","
}

if (options.isPrettyPrintOn) {
if (isPendingSeparator && !IonTextUtils.isAllWhitespace(separatorCharacter)) {
// Only bother if the separator is non-whitespace.
output.appendAscii(separatorCharacter)
}
if (isPendingSeparator || isPendingLeadingWhitespace) {
output.appendAscii(options.lineSeparator())
output.appendAscii(" ".repeat(ancestorContainersStack.size * 2))
}
} else if (isPendingSeparator) {
output.appendAscii(separatorCharacter)
}

isPendingSeparator = false

if (hasFieldName) {
if (fieldNameText != null) {
output.printSymbol(fieldNameText)
output.appendAscii(':')
if (options.isPrettyPrintOn) output.appendAscii(" ")
fieldNameText = null
} else {
output.appendAscii("$")
output.printInt(fieldNameId.toLong())
output.appendAscii(":")
if (options.isPrettyPrintOn) output.appendAscii(" ")
fieldNameId = -1
}
}

for (i in 0 until numAnnotations) {
if (annotationsTextBuffer[i] != null) {
output.printSymbol(annotationsTextBuffer[i])
annotationsTextBuffer[i] = null
} else {
output.appendAscii("$")
output.printInt(annotationsIdBuffer[i].toLong())
annotationsIdBuffer[i] = -1
}
output.appendAscii("::")
}

hasFieldName = false
numAnnotations = 0
valueWriterExpression()
}

private inline fun closeValue(valueWriterExpression: () -> Unit) {
valueWriterExpression()
isPendingSeparator = true
isPendingLeadingWhitespace = false
currentContainerHasValues = true
}

private inline fun writeScalar(valueWriterExpression: () -> Unit) {
// Note—it doesn't matter which order we combine these. The result will be the same because of where
// valueWriterExpression is called in openValue and closeValue.
openValue { closeValue(valueWriterExpression) }
}

override fun close() {
if (closed) return
finish()
output.close()
closed = true
}

override fun finish() {
if (closed) return
confirm(depth() == 0) { "Cannot call finish() while in a container" }
confirm(numAnnotations == 0) { "Cannot call finish with dangling annotations" }
}

override fun writeIVM() {
confirm(currentContainer == Top) { "IVM can only be written at the top level of an Ion stream." }
confirm(numAnnotations == 0) { "Cannot write an IVM with annotations" }
output.appendAscii(IVM)
isPendingSeparator = true
}

override fun isInStruct(): Boolean = currentContainer == Struct

override fun depth(): Int = ancestorContainersStack.size

/**
* Ensures that there is enough space in the annotation buffers for [n] annotations.
* If more space is needed, it over-allocates by 8 to ensure that we're not continually allocating when annotations
* are being added one by one.
*/
private inline fun ensureAnnotationSpace(n: Int) {
// We only need to check the size of one of the arrays because we always keep them the same size.
if (annotationsIdBuffer.size < n) {
val oldIds = annotationsIdBuffer
annotationsIdBuffer = IntArray(n + 8)
oldIds.copyInto(annotationsIdBuffer)
val oldText = annotationsTextBuffer
annotationsTextBuffer = arrayOfNulls(n + 8)
oldText.copyInto(annotationsTextBuffer)
}
}

override fun writeAnnotations(annotation0: Int) {
ensureAnnotationSpace(numAnnotations + 1)
annotationsIdBuffer[numAnnotations++] = annotation0
}

override fun writeAnnotations(annotation0: Int, annotation1: Int) {
ensureAnnotationSpace(numAnnotations + 2)
annotationsIdBuffer[numAnnotations++] = annotation0
annotationsIdBuffer[numAnnotations++] = annotation1
}

override fun writeAnnotations(annotations: IntArray) {
ensureAnnotationSpace(numAnnotations + annotations.size)
annotations.copyInto(annotationsIdBuffer, numAnnotations)
numAnnotations += annotations.size
}

override fun writeAnnotations(annotation0: CharSequence) {
ensureAnnotationSpace(numAnnotations + 1)
annotationsTextBuffer[numAnnotations++] = annotation0
}

override fun writeAnnotations(annotation0: CharSequence, annotation1: CharSequence) {
ensureAnnotationSpace(numAnnotations + 2)
annotationsTextBuffer[numAnnotations++] = annotation0
annotationsTextBuffer[numAnnotations++] = annotation1
}

override fun writeAnnotations(annotations: Array<CharSequence>) {
if (annotations.isEmpty()) return
ensureAnnotationSpace(numAnnotations + annotations.size)
annotations.copyInto(annotationsTextBuffer, numAnnotations)
numAnnotations += annotations.size
}

override fun writeFieldName(sid: Int) {
confirm(currentContainer == Struct) { "Cannot write field name outside of a struct." }
confirm(!hasFieldName) { "Field name already set." }
fieldNameId = sid
hasFieldName = true
}

override fun writeFieldName(text: CharSequence) {
confirm(currentContainer == Struct) { "Cannot write field name outside of a struct." }
confirm(!hasFieldName) { "Field name already set." }
fieldNameText = text
hasFieldName = true
}

override fun writeNull() = writeScalar {
output.appendAscii("null")
}

override fun writeNull(type: IonType) = writeScalar {
val nullimage = if (options._untyped_nulls) { "null" } else {
when (type) {
IonType.NULL -> "null"
IonType.BOOL -> "null.bool"
IonType.INT -> "null.int"
IonType.FLOAT -> "null.float"
IonType.DECIMAL -> "null.decimal"
IonType.TIMESTAMP -> "null.timestamp"
IonType.SYMBOL -> "null.symbol"
IonType.STRING -> "null.string"
IonType.BLOB -> "null.blob"
IonType.CLOB -> "null.clob"
IonType.SEXP -> "null.sexp"
IonType.LIST -> "null.list"
IonType.STRUCT -> "null.struct"
else -> throw IllegalStateException("unexpected type $type")
}
}
output.appendAscii(nullimage)
}

override fun writeBool(value: Boolean) = writeScalar { output.appendAscii(if (value) "true" else "false") }

override fun writeInt(value: Long) = writeScalar { output.printInt(value) }
override fun writeInt(value: BigInteger) = writeScalar { output.printInt(value) }

override fun writeFloat(value: Float) = writeFloat(value.toDouble())
override fun writeFloat(value: Double) = writeScalar { output.printFloat(options, value) }

override fun writeDecimal(value: BigDecimal) = writeScalar { output.printDecimal(options, value) }

override fun writeTimestamp(value: Timestamp) = writeScalar {
writeTimestampHelper(
toMillis = { value.millis },
toString = { value.toString() },
)
}

override fun writeTimestamp(value: Instant) = writeScalar {
writeTimestampHelper(
toMillis = { value.toEpochMilli() },
toString = { value.toString() },
)
}

private inline fun writeTimestampHelper(toMillis: () -> Long, toString: () -> String) {
if (options._timestamp_as_millis) {
output.appendAscii("${toMillis()}")
} else if (options._timestamp_as_string) {
// Timestamp is ASCII-safe so this is easy
output.appendAscii('"')
output.appendAscii(toString())
output.appendAscii('"')
} else {
output.appendAscii(toString())
}
}

override fun writeSymbol(id: Int) = writeScalar {
output.appendAscii('$')
output.printInt(id.toLong())
}

override fun writeSymbol(text: CharSequence) = writeScalar {
when (IonTextUtils.symbolVariant(text)) {
IonTextUtils.SymbolVariant.IDENTIFIER -> output.appendAscii(text)
IonTextUtils.SymbolVariant.OPERATOR -> if (currentContainer == SExp) output.appendAscii(text) else output.printQuotedSymbol(text)
IonTextUtils.SymbolVariant.QUOTED -> output.printQuotedSymbol(text)
}
}

override fun writeString(value: CharSequence) = writeScalar { output.printString(value) }

override fun writeBlob(value: ByteArray, start: Int, length: Int) = writeScalar { output.printBlob(options, value, start, length) }

override fun writeClob(value: ByteArray, start: Int, length: Int) = writeScalar { output.printClob(options, value, start, length) }

override fun stepInList(delimited: Boolean) {
openValue { output.appendAscii("[") }
ancestorContainersStack.add(currentContainer)
currentContainer = List
currentContainerHasValues = false
isPendingLeadingWhitespace = true
}

override fun stepInSExp(delimited: Boolean) {
openValue { output.appendAscii("(") }
ancestorContainersStack.add(currentContainer)
currentContainer = SExp
currentContainerHasValues = false
isPendingLeadingWhitespace = true
}

override fun stepInStruct(delimited: Boolean) {
openValue { output.appendAscii("{") }
ancestorContainersStack.add(currentContainer)
currentContainer = Struct
currentContainerHasValues = false
isPendingLeadingWhitespace = true
}

override fun stepInEExp(name: CharSequence) {
confirm(numAnnotations == 0) { "Cannot annotate a macro invocation" }
openValue {
output.appendAscii("(:")
output.printSymbol(name)
}
ancestorContainersStack.add(currentContainer)
currentContainer = Macro
currentContainerHasValues = false
isPendingSeparator = true // Treat the macro name as if it is a value that needs a separator.
}

override fun stepInEExp(id: Int) {
confirm(numAnnotations == 0) { "Cannot annotate a macro invocation" }
openValue {
output.appendAscii("(:")
output.printInt(id.toLong())
}
ancestorContainersStack.add(currentContainer)
currentContainer = Macro
currentContainerHasValues = false
isPendingSeparator = true // Treat the macro id as if it is a value that needs a separator.
}

override fun stepInExpressionGroup(delimited: Boolean) {
confirm(numAnnotations == 0) { "Cannot annotate an expression group" }
confirm(currentContainer == Macro) { "Can only create an expression group in a macro invocation" }
openValue { output.appendAscii("[:") }
ancestorContainersStack.add(currentContainer)
currentContainer = ExpressionGroup
currentContainerHasValues = false
isPendingLeadingWhitespace = true
}

override fun stepOut() {
confirm(numAnnotations == 0) { "Cannot step out with a dangling annotation" }
confirm(!hasFieldName) { "Cannot step out with a dangling field name" }
val endChar = when (currentContainer) {
Struct -> '}'
SExp, Macro -> ')'
List, ExpressionGroup -> ']'
Top -> throw IonException("Nothing to step out of.")
}

currentContainer = ancestorContainersStack.removeLast()

closeValue {
if (options.isPrettyPrintOn && currentContainerHasValues) {
output.appendAscii(options.lineSeparator())
output.appendAscii(" ".repeat(ancestorContainersStack.size * 2))
}
output.appendAscii(endChar)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import java.time.Instant
*
* This interface allows the user to write Ion data without being concerned about which output format is being used.
*/
interface IonWriter_1_1 {
interface IonRawWriter_1_1 {

/**
* Indicates that writing is completed and all buffered data should be written and flushed as if this were the end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class IonRawBinaryWriter_1_1 internal constructor(
private val out: ByteArrayOutputStream,
private val buffer: WriteBuffer,
private val lengthPrefixPreallocation: Int,
) : IonWriter_1_1 {
) : IonRawWriter_1_1 {

/**
* Types of encoding containers.
Expand Down
Loading

0 comments on commit 82d4497

Please sign in to comment.