diff --git a/src/main/java/com/amazon/ion/IonWriter.java b/src/main/java/com/amazon/ion/IonWriter.java index 93f3fd3ca..8babde9ac 100644 --- a/src/main/java/com/amazon/ion/IonWriter.java +++ b/src/main/java/com/amazon/ion/IonWriter.java @@ -2,9 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 package com.amazon.ion; +import com.amazon.ion.eexp.EExpression; import com.amazon.ion.facet.Faceted; import com.amazon.ion.system.IonTextWriterBuilder; import com.amazon.ion.util.IonStreamUtils; + import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.Flushable; @@ -204,6 +206,7 @@ public interface IonWriter */ public void setFieldNameSymbol(SymbolToken name); + public boolean isFieldNameSet(); /** * Sets the full list of pending annotations to the given text symbols. @@ -280,6 +283,8 @@ public interface IonWriter public void stepOut() throws IOException; + public int getDepth(); + /** * Determines whether values are being written as fields of a struct. * This is especially useful when it is not clear whether field names need @@ -509,4 +514,8 @@ public void writeBlob(byte[] value, int start, int len) public default void writeObject(WriteAsIon obj) { obj.writeTo(this); } + + public default void writeEExpression(EExpression eExpression) { + throw new UnsupportedOperationException("writeEExpression not supported by Ion 1.0 writers."); + } } diff --git a/src/main/java/com/amazon/ion/MacroAwareIonWriter.kt b/src/main/java/com/amazon/ion/MacroAwareIonWriter.kt index 633931d01..9d4b3a050 100644 --- a/src/main/java/com/amazon/ion/MacroAwareIonWriter.kt +++ b/src/main/java/com/amazon/ion/MacroAwareIonWriter.kt @@ -8,18 +8,21 @@ import com.amazon.ion.impl.macro.* * Extension of the IonWriter interface that supports writing macros. * * TODO: Consider exposing this as a Facet. - * - * TODO: See if we can have some sort of safe reference to a macro. */ interface MacroAwareIonWriter : IonWriter { /** + * TODO: This should be internal-only, I think. + * * Starts a new encoding segment with an Ion version marker, flushing * the previous segment (if any) and resetting the encoding context. */ fun startEncodingSegmentWithIonVersionMarker() /** + * + * TODO: This should be internal-only, I think. + * * Starts a new encoding segment with an encoding directive, flushing * the previous segment (if any). * @param macros the macros added in the new segment. diff --git a/src/main/java/com/amazon/ion/WriteAsIon.kt b/src/main/java/com/amazon/ion/WriteAsIon.kt index c19576213..4adbd4dff 100644 --- a/src/main/java/com/amazon/ion/WriteAsIon.kt +++ b/src/main/java/com/amazon/ion/WriteAsIon.kt @@ -2,15 +2,69 @@ // SPDX-License-Identifier: Apache-2.0 package com.amazon.ion +import com.amazon.ion.eexp.* +import com.amazon.ion.impl.macro.* + /** * Indicates that the implementing class has a standardized/built-in way to serialize as Ion. + * + * Example implementation: + * + * ```kotlin + * data class Point2D(val x: Long, val y: Long) : WriteAsIon { + * companion object { + * // This is a very long macro name, but by using the qualified class name, + * // there is almost no risk of having a name conflict with another macro. + * private val MACRO_NAME = Point2D::class.simpleName!!.replace(".", "_") + * private val MACRO = TemplateMacro( + * signature = listOf(exactlyOneTagged("x"), exactlyOneTagged("y")), + * templateBody { + * struct { + * fieldName("x"); variable(0) + * fieldName("y"); variable(1) + * } + * } + * ) + * } + * + * override fun writeWithEExpression(builder: EExpressionBuilder): EExpression? { + * return builder.withName(MACRO_NAME) + * .withMacro(MACRO) + * .withIntArgument(x) + * .withIntArgument(y) + * .build() + * } + * + * override fun writeTo(writer: IonWriter) { + * with(writer) { + * stepIn(IonType.STRUCT) + * setFieldName("x"); writeInt(x) + * setFieldName("x"); writeInt(y) + * stepOut() + * } + * } + * } + * ``` + * + * TODO: There is a significant weakness in this API—if someone calls `myObject.writeTo(myWriter)`, then the check for + * e-expression support is completely bypassed. */ interface WriteAsIon { /** - * Writes this object to an IonWriter capable of producing macro invocations. + * Writes this value to an IonWriter using an E-Expression. + * + * Implementations must return an instance of [EExpression] to indicate that it can be written using an e-expression. + * Returning `null` indicates that this implementation or instance should not be serialized + * + * [EExpression] instances can be obtained from an [EExpressionArgumentBuilder], which in turn, can be obtained by + * calling [Macro.createInvocation] or by using the supplied [builder] in this method. + * + * If you call any methods on [builder], but do not return the [EExpression] produced by [builder], something bad + * will happen. If you're lucky, an exception will be thrown. If you're unlucky, your application will continue + * running and produce invalid or incorrect data. */ - fun writeToMacroAware(writer: MacroAwareIonWriter) = writeTo(writer as IonWriter) + fun writeWithEExpression(builder: EExpressionBuilder): EExpression? = null /** * Writes this object to a standard [IonWriter]. diff --git a/src/main/java/com/amazon/ion/eexp/ArgumentValidatingIonWriterDecorator.kt b/src/main/java/com/amazon/ion/eexp/ArgumentValidatingIonWriterDecorator.kt new file mode 100644 index 000000000..75942f41c --- /dev/null +++ b/src/main/java/com/amazon/ion/eexp/ArgumentValidatingIonWriterDecorator.kt @@ -0,0 +1,245 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.eexp + +import com.amazon.ion.* +import com.amazon.ion.impl.* +import com.amazon.ion.impl.macro.* +import java.math.BigDecimal +import java.math.BigInteger +import java.util.Date + +/** + * A decorator for IonWriter that validates arguments against macro parameter specifications. + * + * This class wraps an IonWriter and ensures that all values written through it conform to + * the constraints specified by a macro parameter. + */ +class ArgumentValidatingIonWriterDecorator( + private val parameter: Macro.Parameter, + private val delegate: IonWriter, +) : IonWriter { + + private val encoding: Macro.ParameterEncoding = parameter.type + + override fun close() {} + override fun flush() {} + override fun finish() {} + override fun asFacet(facetType: Class?): T? = null + override fun getSymbolTable(): SymbolTable? = delegate.symbolTable + + var numberOfExpressions = 0 + private set + + private var depth = 0 + + override fun setFieldName(name: String?) = delegate.setFieldName(name) + override fun setFieldNameSymbol(name: SymbolToken?) = delegate.setFieldNameSymbol(name) + override fun isFieldNameSet(): Boolean = delegate.isFieldNameSet + + override fun setTypeAnnotations(vararg annotations: String?) { + if (depth == 0 && annotations.isNotEmpty()) require(encoding == Macro.ParameterEncoding.Tagged) { "Parameter with encoding $encoding cannot be annotated." } + delegate.setTypeAnnotations(*annotations) + } + + override fun setTypeAnnotationSymbols(vararg annotations: SymbolToken?) { + if (depth == 0 && annotations.isNotEmpty()) require(encoding == Macro.ParameterEncoding.Tagged) { "Parameter with encoding $encoding cannot be annotated." } + delegate.setTypeAnnotationSymbols(*annotations) + } + + override fun addTypeAnnotation(annotation: String?) { + if (depth == 0) require(encoding == Macro.ParameterEncoding.Tagged) { "Parameter with encoding $encoding cannot be annotated." } + delegate.addTypeAnnotation(annotation) + } + + override fun stepIn(containerType: IonType?) { + when (containerType) { + IonType.LIST, IonType.SEXP, IonType.STRUCT -> { + if (depth == 0) { + require(encoding == Macro.ParameterEncoding.Tagged) { "Type $containerType is not valid for parameter encoding $encoding." } + numberOfExpressions++ + } + delegate.stepIn(containerType) + depth++ + } + else -> throw IonException("Not a container type: $containerType") + } + } + + override fun stepOut() { + if (depth == 0) throw IonException("Nothing to step out from") + depth-- + } + + override fun getDepth(): Int = depth + + override fun isInStruct(): Boolean = delegate.isInStruct() + + @Deprecated("Deprecated in IonWriter", ReplaceWith("value.writeTo(this)")) + override fun writeValue(value: IonValue?) { + value?.writeTo(this) + } + + override fun writeValue(reader: IonReader) { + DefaultReaderToWriterTransfer.writeValue(reader, this) + } + + override fun writeValues(reader: IonReader) { + if (reader.type == null) reader.next() + while (reader.type != null) { + writeValue(reader) + reader.next() + } + } + + override fun writeNull() { + if (depth == 0) { + require(encoding == Macro.ParameterEncoding.Tagged) { "null value is not valid for parameter encoding $encoding." } + numberOfExpressions++ + } + delegate.writeNull() + } + + override fun writeNull(type: IonType?) { + if (depth == 0) { + require(encoding == Macro.ParameterEncoding.Tagged) { "null.${type.toString().lowercase()} value is not valid for parameter encoding $encoding." } + numberOfExpressions++ + } + delegate.writeNull(type) + } + + override fun writeBool(value: Boolean) { + if (depth == 0) { + require(encoding == Macro.ParameterEncoding.Tagged) { "bool value is not valid for parameter encoding $encoding." } + numberOfExpressions++ + } + delegate.writeBool(value) + } + + override fun writeInt(value: Long) { + if (depth == 0) { + checkArgumentEncodingCompatibility(parameter, value) + numberOfExpressions++ + } + delegate.writeInt(value) + } + + override fun writeInt(value: BigInteger?) { + if (depth == 0) { + checkArgumentEncodingCompatibility(parameter, value) + numberOfExpressions++ + } + delegate.writeInt(value) + } + + override fun writeFloat(value: Double) { + if (depth == 0) { + checkArgumentEncodingCompatibility(parameter, value) + numberOfExpressions++ + } + delegate.writeFloat(value) + } + + override fun writeDecimal(value: BigDecimal?) { + if (depth == 0) { + require(encoding == Macro.ParameterEncoding.Tagged) { "decimal value is not valid for parameter encoding $encoding." } + numberOfExpressions++ + } + delegate.writeDecimal(value) + } + + override fun writeTimestamp(value: Timestamp?) { + if (depth == 0) { + require(encoding == Macro.ParameterEncoding.Tagged) { "timestamp value is not valid for parameter encoding $encoding." } + numberOfExpressions++ + } + delegate.writeTimestamp(value) + } + + override fun writeTimestampUTC(value: Date?) { + if (depth == 0) { + require(encoding == Macro.ParameterEncoding.Tagged) { "timestamp value is not valid for parameter encoding $encoding." } + numberOfExpressions++ + } + delegate.writeTimestampUTC(value) + } + + override fun writeSymbol(content: String?) { + if (depth == 0) { + when (encoding) { + Macro.ParameterEncoding.Tagged -> delegate.writeSymbol(content) + Macro.ParameterEncoding.FlexString -> { + if (content == null) throw IllegalArgumentException("null.symbol is not valid for parameter encoding $encoding") + delegate.writeSymbol(content) + } + else -> throw IllegalArgumentException("symbol value is not valid for parameter encoding $encoding") + } + numberOfExpressions++ + } + delegate.writeSymbol(content) + } + + override fun writeSymbolToken(content: SymbolToken?) { + if (depth == 0) { + when (encoding) { + Macro.ParameterEncoding.Tagged -> delegate.writeSymbolToken(content) + Macro.ParameterEncoding.FlexString -> { + if (content == null) throw IllegalArgumentException("null.symbol is not valid for parameter encoding $encoding") + TODO() + } + else -> throw IllegalArgumentException("symbol value is not valid for parameter encoding $encoding") + } + numberOfExpressions++ + } + delegate.writeSymbolToken(content) + } + + override fun writeString(value: String?) { + if (depth == 0) { + checkArgumentEncodingCompatibility(parameter, value) + numberOfExpressions++ + } + delegate.writeString(value) + } + + override fun writeClob(value: ByteArray?) { + if (depth == 0) { + require(encoding == Macro.ParameterEncoding.Tagged) { "clob value is not valid for parameter encoding $encoding." } + numberOfExpressions++ + } + delegate.writeClob(value) + } + + override fun writeClob(value: ByteArray?, start: Int, len: Int) { + if (depth == 0) { + require(encoding == Macro.ParameterEncoding.Tagged) { "clob value is not valid for parameter encoding $encoding." } + numberOfExpressions++ + } + delegate.writeClob(value, start, len) + } + + override fun writeBlob(value: ByteArray?) { + if (depth == 0) { + require(encoding == Macro.ParameterEncoding.Tagged) { "blob value is not valid for parameter encoding $encoding." } + numberOfExpressions++ + } + delegate.writeBlob(value) + } + + override fun writeBlob(value: ByteArray?, start: Int, len: Int) { + if (depth == 0) { + require(encoding == Macro.ParameterEncoding.Tagged) { "blob value is not valid for parameter encoding $encoding." } + numberOfExpressions++ + } + delegate.writeBlob(value, start, len) + } + + override fun writeObject(obj: WriteAsIon?) { + if (depth == 0) { + // TODO: How to correctly validate this? Can we defer it until we implement macro-shaped parameters? +// require(encoding == Macro.ParameterEncoding.Tagged) { "e-expression is not valid for parameter encoding $encoding." } + numberOfExpressions++ + } + delegate.writeObject(obj) + } +} diff --git a/src/main/java/com/amazon/ion/eexp/DirectEExpression.kt b/src/main/java/com/amazon/ion/eexp/DirectEExpression.kt new file mode 100644 index 000000000..2f02e3f81 --- /dev/null +++ b/src/main/java/com/amazon/ion/eexp/DirectEExpression.kt @@ -0,0 +1,12 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.eexp + +/** + * This object serves as a sentinel/nonce type to help users of the library correctly + * use the Argument Builder APIs. It enforces type safety and proper usage patterns + * in the fluent builder API for direct encoding expressions. + * + * See [DirectEExpressionArgumentBuilder] and [WriteAsIon][com.amazon.ion.WriteAsIon] + */ +object DirectEExpression : EExpression diff --git a/src/main/java/com/amazon/ion/eexp/DirectEExpressionArgumentBuilder.kt b/src/main/java/com/amazon/ion/eexp/DirectEExpressionArgumentBuilder.kt new file mode 100644 index 000000000..9124a1949 --- /dev/null +++ b/src/main/java/com/amazon/ion/eexp/DirectEExpressionArgumentBuilder.kt @@ -0,0 +1,112 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.eexp + +import com.amazon.ion.* +import com.amazon.ion.impl.bin.* +import com.amazon.ion.impl.macro.* +import java.math.BigInteger +import java.util.function.Consumer + +/** + * An implementation of [EExpressionArgumentBuilder] that directly writes encoding expressions. + * + * This builder handles the immediate writing of encoding expression arguments to an Ion writer. + * It validates arguments against the macro's parameter specifications and ensures proper encoding + * based on the parameter types and cardinality constraints. + * + * @property macro The macro definition that specifies the parameters and their constraints + * @property managedWriter The Ion writer to which the encoding expression will be written + */ +internal class DirectEExpressionArgumentBuilder( + private val macro: Macro, + private val managedWriter: IonManagedWriter_1_1, +) : EExpressionArgumentBuilder { + + private var i = 0 + + override fun withAbsentArgument(): EExpressionArgumentBuilder { + val parameter = macro.signature[i++] + require(parameter.cardinality.canBeVoid) { "Parameter ${parameter.variableName} requires an argument" } + managedWriter.startExpressionGroup() + managedWriter.endExpressionGroup() + return this + } + + override fun withIntArgument(value: Long): EExpressionArgumentBuilder { + val parameter = macro.signature[i++] + checkArgumentEncodingCompatibility(parameter, value) + managedWriter.writeInt(value) + return this + } + + override fun withIntArgument(value: BigInteger): EExpressionArgumentBuilder { + // TODO: Some of this logic is partially duplicated in the binary raw writer. + // Once the writer APIs are stabilized, consolidate the logic to a single location. + val parameter = macro.signature[i++] + checkArgumentEncodingCompatibility(parameter, value) + managedWriter.writeInt(value) + return this + } + + override fun withFloatArgument(value: Double): EExpressionArgumentBuilder { + val parameter = macro.signature[i++] + checkArgumentEncodingCompatibility(parameter, value) + managedWriter.writeFloat(value) + return this + } + + override fun withStringArgument(value: String): EExpressionArgumentBuilder { + val parameter = macro.signature[i++] + val encoding = parameter.type + when (encoding) { + Macro.ParameterEncoding.Tagged, + Macro.ParameterEncoding.FlexString -> managedWriter.writeString(value) + else -> throw IllegalArgumentException("Parameter ${parameter.variableName} must be a ${parameter.type.ionTextName}") + } + return this + } + + override fun withArgument(values: Consumer): EExpressionArgumentBuilder { + val parameter = macro.signature[i++] + // require(parameter.type == Macro.ParameterEncoding.Tagged) { "Parameter ${parameter.variableName} must be a ${parameter.type.ionTextName}" } + + val validatedArgumentWriter = ArgumentValidatingIonWriterDecorator(parameter, managedWriter) + + when (parameter.cardinality) { + Macro.ParameterCardinality.ZeroOrOne -> { + managedWriter.startExpressionGroup() + values.accept(validatedArgumentWriter) + managedWriter.endExpressionGroup() + val numberOfArguments = validatedArgumentWriter.numberOfExpressions + require(numberOfArguments <= 1) { "Parameter ${parameter.variableName} must have 0 or 1 arguments" } + } + Macro.ParameterCardinality.ExactlyOne -> { + values.accept(validatedArgumentWriter) + val numberOfArguments = validatedArgumentWriter.numberOfExpressions + require(numberOfArguments == 1) { "Parameter ${parameter.variableName} must have exactly 1 argument" } + } + Macro.ParameterCardinality.OneOrMore -> { + managedWriter.startExpressionGroup() + values.accept(validatedArgumentWriter) + managedWriter.endExpressionGroup() + val numberOfArguments = validatedArgumentWriter.numberOfExpressions + require(numberOfArguments > 0) { "Parameter ${parameter.variableName} must have 1 or more arguments" } + } + Macro.ParameterCardinality.ZeroOrMore -> { + managedWriter.startExpressionGroup() + values.accept(validatedArgumentWriter) + managedWriter.endExpressionGroup() + } + } + return this + } + + override fun build(): DirectEExpression { + for (j in i until macro.signature.size) { + withAbsentArgument() + } + managedWriter.endMacro() + return DirectEExpression + } +} diff --git a/src/main/java/com/amazon/ion/eexp/DirectEExpressionBuilder.kt b/src/main/java/com/amazon/ion/eexp/DirectEExpressionBuilder.kt new file mode 100644 index 000000000..be4be4734 --- /dev/null +++ b/src/main/java/com/amazon/ion/eexp/DirectEExpressionBuilder.kt @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.eexp + +import com.amazon.ion.impl.bin.* +import com.amazon.ion.impl.macro.* + +/** + * A builder for creating direct encoding expressions. + * + * This implementation writes encoding expressions directly to an Ion writer as they + * are built, rather than storing them for later use. + */ +class DirectEExpressionBuilder internal constructor(private val writer: IonManagedWriter_1_1) : EExpressionBuilder { + private var name: String? = null + + override fun withName(name: String?): DirectEExpressionBuilder { + this.name = name + return this + } + + override fun withMacro(macro: Macro): EExpressionArgumentBuilder { + val name = name + if (name != null) { + writer.startMacro(name, macro) + } else { + writer.startMacro(macro) + } + return DirectEExpressionArgumentBuilder(macro, writer) + } +} diff --git a/src/main/java/com/amazon/ion/eexp/EExpression.kt b/src/main/java/com/amazon/ion/eexp/EExpression.kt new file mode 100644 index 000000000..6fff82a35 --- /dev/null +++ b/src/main/java/com/amazon/ion/eexp/EExpression.kt @@ -0,0 +1,14 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.eexp + +/** + * Represents an Encoding Expression (EExpression) in Ion. + * + * An EExpression is a construct that enables encoding-specific functionality within Ion data, + * providing a mechanism for specialized data encoding and transformation operations. + * + * This interface serves as a marker for all EExpression implementations and is used + * by the encoding expression evaluation system. + */ +interface EExpression diff --git a/src/main/java/com/amazon/ion/eexp/EExpressionArgumentBuilder.kt b/src/main/java/com/amazon/ion/eexp/EExpressionArgumentBuilder.kt new file mode 100644 index 000000000..da58621d4 --- /dev/null +++ b/src/main/java/com/amazon/ion/eexp/EExpressionArgumentBuilder.kt @@ -0,0 +1,42 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.eexp + +import com.amazon.ion.IonWriter +import java.math.BigInteger +import java.util.function.Consumer + +interface EExpressionArgumentBuilder { + + fun withAbsentArgument(): EExpressionArgumentBuilder + + fun withIntArgument(value: Byte): EExpressionArgumentBuilder = withIntArgument(value.toLong()) + fun withIntArgument(value: Short): EExpressionArgumentBuilder = withIntArgument(value.toLong()) + fun withIntArgument(value: Int): EExpressionArgumentBuilder = withIntArgument(value.toLong()) + fun withIntArgument(value: Long): EExpressionArgumentBuilder + fun withIntArgument(value: BigInteger): EExpressionArgumentBuilder + + fun withFloatArgument(value: Float): EExpressionArgumentBuilder = withFloatArgument(value.toDouble()) + fun withFloatArgument(value: Double): EExpressionArgumentBuilder + + /* + TODO: Methods that are optimized for writing tagless groups. + Eg: + fun withIntArgGroup(values: ByteArray): ArgumentBuilder + fun withIntArgGroup(values: ShortArray): ArgumentBuilder + fun withIntArgGroup(values: IntArray): ArgumentBuilder + fun withIntArgGroup(values: LongArray): ArgumentBuilder + fun withFloatArgGroup(values: FloatArray): ArgumentBuilder + fun withFloatArgGroup(values: DoubleArray): ArgumentBuilder + */ + + // TODO: + // fun withSymbolArgument(content: String): ArgumentBuilder + // ... but we need to support SymbolTokens as well. + + fun withStringArgument(value: String): EExpressionArgumentBuilder + + fun withArgument(values: Consumer): EExpressionArgumentBuilder + + fun build(): T +} diff --git a/src/main/java/com/amazon/ion/eexp/EExpressionBuilder.kt b/src/main/java/com/amazon/ion/eexp/EExpressionBuilder.kt new file mode 100644 index 000000000..aa992c556 --- /dev/null +++ b/src/main/java/com/amazon/ion/eexp/EExpressionBuilder.kt @@ -0,0 +1,29 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.eexp + +import com.amazon.ion.impl.macro.* + +/** + * Builder interface for creating Encoding Expressions (EExpressions). + * + * This interface provides a fluent API for constructing EExpressions by specifying + * their name and associated macro definitions. + */ +interface EExpressionBuilder { + /** + * Sets the name for the EExpression being built. + * + * @param name The name to assign to the EExpression, or null for an anonymous expression + * @return This builder for method chaining + */ + fun withName(name: String?): EExpressionBuilder + + /** + * Associates a macro with this EExpression and transitions to argument building. + * + * @param macro The macro definition to use for this EExpression + * @return An argument builder for specifying the macro's arguments + */ + fun withMacro(macro: Macro): EExpressionArgumentBuilder +} diff --git a/src/main/java/com/amazon/ion/eexp/IonWriterRecorder.kt b/src/main/java/com/amazon/ion/eexp/IonWriterRecorder.kt new file mode 100644 index 000000000..10450dd5c --- /dev/null +++ b/src/main/java/com/amazon/ion/eexp/IonWriterRecorder.kt @@ -0,0 +1,189 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.eexp + +import com.amazon.ion.* +import com.amazon.ion.impl.* +import java.math.BigDecimal +import java.math.BigInteger +import java.util.ArrayDeque +import java.util.Date +import java.util.Deque + +/** + * An Ion Writer that records write operations for later replay. + * + * This class captures Ion write operations without immediately executing them, + * storing them for later replay on another IonWriter. + */ +class IonWriterRecorder : IonWriter { + + override fun close() {} + override fun flush() {} + override fun finish() {} + override fun asFacet(facetType: Class?): T? = null + override fun getSymbolTable(): SymbolTable? = null + + private var depth = 0 + + private val containerStack: Deque = ArrayDeque() + + private val actions = ArrayList Unit>() + + private var isFieldNameSet = false + + fun replay(target: IonWriter) { + actions.forEach { it(target) } + } + + override fun setFieldName(name: String?) { + if (!isInStruct) throw IonException("Cannot set field name when not in struct") + actions.add { setFieldName(name) } + isFieldNameSet = true + } + + override fun setFieldNameSymbol(name: SymbolToken?) { + if (!isInStruct) throw IonException("Cannot set field name when not in struct") + actions.add { setFieldNameSymbol(name) } + isFieldNameSet = true + } + + override fun isFieldNameSet(): Boolean = isFieldNameSet + + override fun setTypeAnnotations(vararg annotations: String?) { + actions.add { setTypeAnnotations(*annotations) } + } + + override fun setTypeAnnotationSymbols(vararg annotations: SymbolToken?) { + actions.add { setTypeAnnotationSymbols(*annotations) } + } + + override fun addTypeAnnotation(annotation: String?) { + actions.add { addTypeAnnotation(annotation) } + } + + override fun stepIn(containerType: IonType?) { + when (containerType) { + IonType.LIST, IonType.SEXP, IonType.STRUCT -> { + actions.add { stepIn(containerType) } + containerStack.push(containerType) + depth++ + } + else -> throw IonException("Not a container type: $containerType") + } + isFieldNameSet = false + } + + override fun stepOut() { + if (depth == 0) throw IonException("Nothing to step out from") + containerStack + depth-- + isFieldNameSet = false + } + + override fun getDepth(): Int = depth + + override fun isInStruct(): Boolean = containerStack.peek() == IonType.STRUCT + + @Deprecated("Deprecated in IonWriter", ReplaceWith("value.writeTo(this)")) + override fun writeValue(value: IonValue?) { + value?.writeTo(this) + } + + override fun writeValue(reader: IonReader) { + DefaultReaderToWriterTransfer.writeValue(reader, this) + } + + override fun writeValues(reader: IonReader) { + if (reader.type == null) reader.next() + while (reader.type != null) { + writeValue(reader) + reader.next() + } + } + + override fun writeNull() { + actions.add { writeNull() } + isFieldNameSet = false + } + + override fun writeNull(type: IonType?) { + actions.add { writeNull(type) } + isFieldNameSet = false + } + + override fun writeBool(value: Boolean) { + actions.add { writeBool(value) } + isFieldNameSet = false + } + + override fun writeInt(value: Long) { + actions.add { writeInt(value) } + isFieldNameSet = false + } + + override fun writeInt(value: BigInteger?) { + actions.add { writeInt(value) } + isFieldNameSet = false + } + + override fun writeFloat(value: Double) { + actions.add { writeFloat(value) } + isFieldNameSet = false + } + + override fun writeDecimal(value: BigDecimal?) { + actions.add { writeDecimal(value) } + isFieldNameSet = false + } + + override fun writeTimestamp(value: Timestamp?) { + actions.add { writeTimestamp(value) } + isFieldNameSet = false + } + + override fun writeTimestampUTC(value: Date?) { + actions.add { writeTimestampUTC(value) } + isFieldNameSet = false + } + + override fun writeSymbol(content: String?) { + actions.add { writeSymbol(content) } + isFieldNameSet = false + } + + override fun writeSymbolToken(content: SymbolToken?) { + actions.add { writeSymbolToken(content) } + isFieldNameSet = false + } + + override fun writeString(value: String?) { + actions.add { writeString(value) } + isFieldNameSet = false + } + + override fun writeClob(value: ByteArray?) { + actions.add { writeClob(value) } + isFieldNameSet = false + } + + override fun writeClob(value: ByteArray?, start: Int, len: Int) { + actions.add { writeClob(value, start, len) } + isFieldNameSet = false + } + + override fun writeBlob(value: ByteArray?) { + actions.add { writeBlob(value) } + isFieldNameSet = false + } + + override fun writeBlob(value: ByteArray?, start: Int, len: Int) { + actions.add { writeBlob(value, start, len) } + isFieldNameSet = false + } + + override fun writeObject(obj: WriteAsIon) { + actions.add { writeObject(obj) } + isFieldNameSet = false + } +} diff --git a/src/main/java/com/amazon/ion/eexp/PreparedEExpression.kt b/src/main/java/com/amazon/ion/eexp/PreparedEExpression.kt new file mode 100644 index 000000000..e3106e167 --- /dev/null +++ b/src/main/java/com/amazon/ion/eexp/PreparedEExpression.kt @@ -0,0 +1,41 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.eexp + +import com.amazon.ion.IonWriter +import com.amazon.ion.WriteAsIon +import com.amazon.ion.impl.macro.Macro + +/** + * A prepared encoding expression that can be reused multiple times. + * + * This class represents a pre-configured encoding expression with its macro name, + * definition, and argument builders. It can be efficiently reused to write the same + * expression multiple times without needing to reconfigure the parameters each time. + * + * @property macroName The name of the macro to be used, or null for anonymous expressions + * @property macroDefinition The macro definition that defines the parameter types and constraints + * @property arguments List of argument builder functions to configure the expression's parameters + */ +class PreparedEExpression private constructor( + val macroName: String?, + val macroDefinition: Macro, + private val arguments: ArrayList.() -> Unit>, +) : WriteAsIon, EExpression { + + internal constructor(builder: PreparedEExpressionArgumentBuilder) : this( + builder.macroName, + builder.macroDefinition, + builder.arguments, + ) + + override fun writeWithEExpression(builder: EExpressionBuilder): EExpression { + val argBuilder = builder.withName(macroName).withMacro(macroDefinition) + arguments.forEach { it(argBuilder) } + return argBuilder.build() + } + + override fun writeTo(writer: IonWriter) { + TODO("Evaluate this e-expression and write as a not an e-expression") + } +} diff --git a/src/main/java/com/amazon/ion/eexp/PreparedEExpressionArgumentBuilder.kt b/src/main/java/com/amazon/ion/eexp/PreparedEExpressionArgumentBuilder.kt new file mode 100644 index 000000000..67d1e5e62 --- /dev/null +++ b/src/main/java/com/amazon/ion/eexp/PreparedEExpressionArgumentBuilder.kt @@ -0,0 +1,87 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.eexp + +import com.amazon.ion.IonWriter +import com.amazon.ion.MacroAwareIonWriter +import com.amazon.ion.impl.macro.Macro +import java.math.BigInteger +import java.util.ArrayList +import java.util.function.Consumer + +/** + * A builder for creating prepared encoding expressions. + * + * This builder collects and validates arguments for a prepared encoding expression, + * which can be reused multiple times. It ensures that all arguments conform to the + * macro's parameter specifications before the expression is built. + * + * The arguments are not written to an Ion stream immediately, but are stored + * (or deferred) for later use. + * + * There is one function call per parameter. Omitted trailing args are implicitly absent. + */ +class PreparedEExpressionArgumentBuilder(val macroName: String?, internal val macroDefinition: Macro) : EExpressionArgumentBuilder { + + override fun build(): PreparedEExpression { + for (j in i until macroDefinition.signature.size) { + withAbsentArgument() + } + return PreparedEExpression(this) + } + + private var i = 0 + internal val arguments = ArrayList.() -> Unit>() + + override fun withAbsentArgument(): PreparedEExpressionArgumentBuilder = apply { + val parameter = macroDefinition.signature[i++] + require(parameter.cardinality.canBeVoid) { "Parameter ${parameter.variableName} requires an argument" } + arguments.add { withAbsentArgument() } + } + + override fun withIntArgument(value: Long): PreparedEExpressionArgumentBuilder = apply { + val parameter = macroDefinition.signature[i++] + checkArgumentEncodingCompatibility(parameter, value) + arguments.add { withIntArgument(value) } + } + + override fun withIntArgument(value: BigInteger): PreparedEExpressionArgumentBuilder = apply { + val parameter = macroDefinition.signature[i++] + checkArgumentEncodingCompatibility(parameter, value) + arguments.add { withIntArgument(value) } + } + + override fun withFloatArgument(value: Double): PreparedEExpressionArgumentBuilder = apply { + val parameter = macroDefinition.signature[i++] + checkArgumentEncodingCompatibility(parameter, value) + arguments.add { withFloatArgument(value) } + } + + override fun withStringArgument(value: String): PreparedEExpressionArgumentBuilder = apply { + val parameter = macroDefinition.signature[i++] + checkArgumentEncodingCompatibility(parameter, value) + arguments.add { withStringArgument(value) } + } + + override fun withArgument(values: Consumer): PreparedEExpressionArgumentBuilder = apply { + val parameter = macroDefinition.signature[i++] + + val recorder = IonWriterRecorder() + val validator = ArgumentValidatingIonWriterDecorator(parameter, recorder) + values.accept(validator) + + val numberOfArguments = validator.numberOfExpressions + when (parameter.cardinality) { + Macro.ParameterCardinality.ZeroOrOne -> require(numberOfArguments <= 1) { "Parameter ${parameter.variableName} must have 0 or 1 arguments" } + Macro.ParameterCardinality.ExactlyOne -> require(numberOfArguments == 1) { "Parameter ${parameter.variableName} must have exactly 1 argument" } + Macro.ParameterCardinality.OneOrMore -> require(numberOfArguments > 0) { "Parameter ${parameter.variableName} must have 1 or more arguments" } + Macro.ParameterCardinality.ZeroOrMore -> {} + } + + if (numberOfArguments == 0) { + arguments.add { withAbsentArgument() } + } else { + arguments.add { withArgument { recorder.replay(it as MacroAwareIonWriter) } } + } + } +} diff --git a/src/main/java/com/amazon/ion/eexp/checkArgumentEncodingCompatibility.kt b/src/main/java/com/amazon/ion/eexp/checkArgumentEncodingCompatibility.kt new file mode 100644 index 000000000..f14123330 --- /dev/null +++ b/src/main/java/com/amazon/ion/eexp/checkArgumentEncodingCompatibility.kt @@ -0,0 +1,86 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.eexp + +import com.amazon.ion.impl.macro.* +import java.math.BigInteger + +/** + * Checks that the encoding of [parameter] can represent the given [Double] value. + */ +internal fun checkArgumentEncodingCompatibility(parameter: Macro.Parameter, value: Double) { + val encoding = parameter.type + val isInRangeForEncoding = when (encoding) { + Macro.ParameterEncoding.Tagged -> true + Macro.ParameterEncoding.Float16 -> TODO("Writing float16 values is not implemented yet") + Macro.ParameterEncoding.Float32 -> value.toFloat().toDouble() == value + Macro.ParameterEncoding.Float64 -> true + else -> throw IllegalArgumentException("Parameter ${parameter.variableName} must be a ${encoding.ionTextName}") + } + require(isInRangeForEncoding) { "Parameter ${parameter.variableName} must be a ${encoding.ionTextName}; value is out of range for encoding type: $value" } +} + +/** + * Checks that the encoding of [parameter] can represent the given [Long] value. + */ +internal fun checkArgumentEncodingCompatibility(parameter: Macro.Parameter, value: Long) { + val encoding = parameter.type + val isInRangeForEncoding = when (encoding) { + Macro.ParameterEncoding.Tagged -> true + Macro.ParameterEncoding.Int8 -> value.toByte().toLong() == value + Macro.ParameterEncoding.Int16 -> value.toShort().toLong() == value + Macro.ParameterEncoding.Int32 -> value.toInt().toLong() == value + Macro.ParameterEncoding.Int64 -> true + Macro.ParameterEncoding.FlexInt -> true + Macro.ParameterEncoding.Uint8 -> value >= 0 && value.toByte().toLong() == value + Macro.ParameterEncoding.Uint16 -> value >= 0 && value.toShort().toLong() == value + Macro.ParameterEncoding.Uint32 -> value >= 0 && value.toInt().toLong() == value + Macro.ParameterEncoding.Uint64 -> value >= 0 + Macro.ParameterEncoding.FlexUint -> value >= 0 + else -> throw IllegalArgumentException("Parameter ${parameter.variableName} must be a ${parameter.type.ionTextName}") + } + require(isInRangeForEncoding) { "Parameter ${parameter.variableName} must be a ${parameter.type.ionTextName}; value is out of range for encoding type: $value" } +} + +/** + * Checks that the encoding of [parameter] can represent the given [BigInteger] value. + */ +internal fun checkArgumentEncodingCompatibility(parameter: Macro.Parameter, value: BigInteger?) { + val encoding = parameter.type + + if (value == null) { + if (encoding == Macro.ParameterEncoding.Tagged) return + throw IllegalArgumentException("Parameter ${parameter.variableName} must be a ${encoding.ionTextName}; value may not be null") + } + + val isInRangeForEncoding = when (encoding) { + Macro.ParameterEncoding.Tagged -> true + Macro.ParameterEncoding.Int8 -> value.bitLength() <= 8 + Macro.ParameterEncoding.Int16 -> value.bitLength() <= 16 + Macro.ParameterEncoding.Int32 -> value.bitLength() <= 32 + Macro.ParameterEncoding.Int64 -> value.bitLength() <= 64 + Macro.ParameterEncoding.FlexInt -> true + Macro.ParameterEncoding.Uint8 -> value.signum() >= 0 && value.bitLength() <= 8 + Macro.ParameterEncoding.Uint16 -> value.signum() >= 0 && value.bitLength() <= 16 + Macro.ParameterEncoding.Uint32 -> value.signum() >= 0 && value.bitLength() <= 32 + Macro.ParameterEncoding.Uint64 -> value.signum() >= 0 && value.bitLength() <= 64 + Macro.ParameterEncoding.FlexUint -> value.signum() >= 0 + else -> throw IllegalArgumentException("Parameter ${parameter.variableName} must be a ${parameter.type.ionTextName}") + } + require(isInRangeForEncoding) { "Parameter ${parameter.variableName} must be a ${parameter.type.ionTextName}; value is out of range for encoding type: $value" } +} + +/** + * Checks that the encoding of [parameter] can represent the given [String] value as an Ion String. + */ +internal fun checkArgumentEncodingCompatibility(parameter: Macro.Parameter, value: String?) { + when (val encoding = parameter.type) { + Macro.ParameterEncoding.Tagged -> {} + Macro.ParameterEncoding.FlexString -> { + if (value == null) { + throw IllegalArgumentException("Parameter ${parameter.variableName} must be a ${encoding.ionTextName}; value may not be null") + } + } + else -> throw IllegalArgumentException("Parameter ${parameter.variableName} must be a ${parameter.type.ionTextName}") + } +} diff --git a/src/main/java/com/amazon/ion/impl/DefaultReaderToWriterTransfer.java b/src/main/java/com/amazon/ion/impl/DefaultReaderToWriterTransfer.java new file mode 100644 index 000000000..afd5a6ffd --- /dev/null +++ b/src/main/java/com/amazon/ion/impl/DefaultReaderToWriterTransfer.java @@ -0,0 +1,167 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.ion.impl; + +import com.amazon.ion.IonReader; +import com.amazon.ion.IonType; +import com.amazon.ion.IonWriter; +import com.amazon.ion.SymbolToken; +import com.amazon.ion.UnknownSymbolException; + +import java.io.IOException; + +public class DefaultReaderToWriterTransfer { + private DefaultReaderToWriterTransfer() {} + + /** + * @throws UnknownSymbolException if the text of the field name is + * unknown. + */ + private static void write_value_field_name_helper(IonReader reader, IonWriter writer) + { + if (writer.isInStruct() && !writer.isFieldNameSet()) + { + SymbolToken tok = reader.getFieldNameSymbol(); + if (tok == null) + { + throw new IllegalStateException("Field name not set"); + } + + writer.setFieldNameSymbol(tok); + } + } + + private static void write_value_annotations_helper(IonReader reader, IonWriter writer) + { + SymbolToken[] a = reader.getTypeAnnotationSymbols(); + // At present, we must always call this, even when the list is empty, + // because local symtab diversion leaves the $ion_symbol_table + // dangling on the system writer! TODO fix that, it's broken. + writer.setTypeAnnotationSymbols(a); + } + + /** + * Overrides can optimize special cases. + */ + public static void writeValue(IonReader reader, IonWriter writer) throws IOException + { + writeValueRecursively(reader, writer); + } + + /** + * Writes the provided IonReader's current value including any annotations. This function will not advance the + * IonReader beyond the end of the current value; users wishing to continue using the IonReader at the current + * depth will need to call {@link IonReader#next()} again. + * + * - If the IonReader is not positioned over a value (for example: because it is at the beginning or end of a + * stream), then this function does nothing. + * - If the current value is a container, this function will visit all of its child values and write those too, + * advancing the IonReader to the end of the container in the process. + * - If both this writer and the IonReader are in a struct, the writer will write the current value's field name. + * - If the writer is not in a struct but the reader is, the writer will ignore the current value's field name. + * - If the writer is in a struct but the IonReader is not, this function throws an IllegalStateException. + * + * @param reader The IonReader that will provide a value to write. + * @throws IOException if either the provided IonReader or this writer's underlying OutputStream throw an + * IOException. + * @throws IllegalStateException if this writer is inside a struct but the IonReader is not. + */ + static void writeValueRecursively(IonReader reader, IonWriter writer) throws IOException + { + // The IonReader does not need to be at the top level (getDepth()==0) when the function is called. + // We take note of its initial depth so we can avoid advancing the IonReader beyond the starting value. + int startingDepth = reader.getDepth(); + + // The IonReader will be at `startingDepth` when the function is first called and then again when we + // have finished traversing all of its children. This boolean tracks which of those two states we are + // in when `getDepth() == startingDepth`. + boolean alreadyProcessedTheStartingValue = false; + + // The IonType of the IonReader's current value. + IonType type; + + while (true) { + // Each time we reach the top of the loop we are in one of three states: + // 1. We have not yet begun processing the starting value. + // 2. We are currently traversing the starting value's children. + // 3. We have finished processing the starting value. + if (reader.getDepth() == startingDepth) { + // The IonReader is at the starting depth. We're either beginning our traversal or finishing it. + if (alreadyProcessedTheStartingValue) { + // We're finishing our traversal. + break; + } + // We're beginning our traversal. Don't advance the cursor; instead, use the current + // value's IonType. + type = reader.getType(); + // We've begun processing the starting value. + alreadyProcessedTheStartingValue = true; + } else { + // We're traversing the starting value's children (that is: values at greater depths). We need to + // advance the cursor by calling next(). + type = reader.next(); + } + + if (type == null) { + // There are no more values at this level. If we're at the starting level, we're done. + if (reader.getDepth() == startingDepth) { + break; + } + // Otherwise, step out once and then try to move forward again. + reader.stepOut(); + writer.stepOut(); + continue; + } + + // We found a value. Write out its field name and annotations, if any. + write_value_field_name_helper(reader, writer); + write_value_annotations_helper(reader, writer); + + if (reader.isNullValue()) { + writer.writeNull(type); + continue; + } + + switch (type) { + case NULL: + // The isNullValue() check above will handle this. + throw new IllegalStateException("isNullValue() was false but IonType was NULL."); + case BOOL: + writer.writeBool(reader.booleanValue()); + break; + case INT: + writer.writeInt(reader.bigIntegerValue()); + break; + case FLOAT: + writer.writeFloat(reader.doubleValue()); + break; + case DECIMAL: + writer.writeDecimal(reader.decimalValue()); + break; + case TIMESTAMP: + writer.writeTimestamp(reader.timestampValue()); + break; + case STRING: + writer.writeString(reader.stringValue()); + break; + case SYMBOL: + writer.writeSymbolToken(reader.symbolValue()); + break; + case BLOB: + writer.writeBlob(reader.newBytes()); + break; + case CLOB: + writer.writeClob(reader.newBytes()); + break; + case STRUCT: // Intentional fallthrough + case LIST: // Intentional fallthrough + case SEXP: + reader.stepIn(); + writer.stepIn(type); + break; + default: + throw new IllegalStateException("Unknown value type: " + type); + } + } + } +} diff --git a/src/main/java/com/amazon/ion/impl/IonCursorBinary.java b/src/main/java/com/amazon/ion/impl/IonCursorBinary.java index df639b503..0f00c819b 100644 --- a/src/main/java/com/amazon/ion/impl/IonCursorBinary.java +++ b/src/main/java/com/amazon/ion/impl/IonCursorBinary.java @@ -1981,6 +1981,8 @@ private void uncheckedSkipMacroParameter(Macro.Parameter parameter) { case FlexSym: uncheckedReadFlexSym_1_1(valueMarker); break; + default: + return; } } diff --git a/src/main/java/com/amazon/ion/impl/IonRawTextWriter_1_1.kt b/src/main/java/com/amazon/ion/impl/IonRawTextWriter_1_1.kt index 146d67d0f..beaf2a8f6 100644 --- a/src/main/java/com/amazon/ion/impl/IonRawTextWriter_1_1.kt +++ b/src/main/java/com/amazon/ion/impl/IonRawTextWriter_1_1.kt @@ -3,7 +3,9 @@ package com.amazon.ion.impl import com.amazon.ion.* +import com.amazon.ion.eexp.* import com.amazon.ion.impl.IonRawTextWriter_1_1.ContainerType.* +import com.amazon.ion.impl.IonRawTextWriter_1_1.ContainerType.EExpression import com.amazon.ion.impl.IonRawTextWriter_1_1.ContainerType.List import com.amazon.ion.impl.bin.* import com.amazon.ion.impl.macro.* diff --git a/src/main/java/com/amazon/ion/impl/IonWriterUserBinary.java b/src/main/java/com/amazon/ion/impl/IonWriterUserBinary.java index b460a1871..f1b2e421b 100644 --- a/src/main/java/com/amazon/ion/impl/IonWriterUserBinary.java +++ b/src/main/java/com/amazon/ion/impl/IonWriterUserBinary.java @@ -1,18 +1,5 @@ -/* - * 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. - */ - +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 package com.amazon.ion.impl; import static com.amazon.ion.impl._Private_Utils.isNonSymbolScalar; @@ -106,7 +93,7 @@ public void writeValue(IonReader reader) // From here on, we won't call back into this method, so we won't // bother doing all those checks again. - writeValueRecursively(reader); + DefaultReaderToWriterTransfer.writeValueRecursively(reader, this); } diff --git a/src/main/java/com/amazon/ion/impl/TaglessEncoding.kt b/src/main/java/com/amazon/ion/impl/TaglessEncoding.kt index 4d6c4e1f9..3e6040a56 100644 --- a/src/main/java/com/amazon/ion/impl/TaglessEncoding.kt +++ b/src/main/java/com/amazon/ion/impl/TaglessEncoding.kt @@ -23,5 +23,6 @@ enum class TaglessEncoding(@JvmField internal val typeID: IonTypeID, @JvmField v FLOAT16(IonTypeID.TYPE_IDS_1_1[0x6B], false), FLOAT32(IonTypeID.TYPE_IDS_1_1[0x6C], false), FLOAT64(IonTypeID.TYPE_IDS_1_1[0x6D], false), - FLEX_SYM(IonTypeID.TYPE_IDS_1_1[0xFA], false) + FLEX_STRING(IonTypeID.TYPE_IDS_1_1[0xF9], false), + FLEX_SYM(IonTypeID.TYPE_IDS_1_1[0xFA], false), } diff --git a/src/main/java/com/amazon/ion/impl/_Private_IonWriterBase.java b/src/main/java/com/amazon/ion/impl/_Private_IonWriterBase.java index 690567253..71ebc48dd 100644 --- a/src/main/java/com/amazon/ion/impl/_Private_IonWriterBase.java +++ b/src/main/java/com/amazon/ion/impl/_Private_IonWriterBase.java @@ -1,18 +1,5 @@ -/* - * 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. - */ - +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 package com.amazon.ion.impl; import com.amazon.ion.IonReader; @@ -59,7 +46,7 @@ public _Private_IonWriterBase(boolean requireSymbolValidation) { * 0 if the writer is at top-level. * @return int depth of container nesting */ - protected abstract int getDepth(); + public abstract int getDepth(); //======================================================================== @@ -320,34 +307,6 @@ private final void transfer_symbol_tables(_Private_ReaderWriter reader) } } - /** - * @throws UnknownSymbolException if the text of the field name is - * unknown. - */ - private final void write_value_field_name_helper(IonReader reader) - { - if (this.isInStruct() && !isFieldNameSet()) - { - SymbolToken tok = reader.getFieldNameSymbol(); - if (tok == null) - { - throw new IllegalStateException("Field name not set"); - } - - setFieldNameSymbol(tok); - } - } - - private final void write_value_annotations_helper(IonReader reader) - { - SymbolToken[] a = reader.getTypeAnnotationSymbols(); - // At present, we must always call this, even when the list is empty, - // because local symtab diversion leaves the $ion_symbol_table - // dangling on the system writer! TODO fix that, it's broken. - setTypeAnnotationSymbols(a); - } - - public boolean isStreamCopyOptimized() { return false; @@ -359,124 +318,7 @@ public boolean isStreamCopyOptimized() public void writeValue(IonReader reader) throws IOException { // TODO this should do symtab optimization as per writeValues() - writeValueRecursively(reader); - } - - /** - * Writes the provided IonReader's current value including any annotations. This function will not advance the - * IonReader beyond the end of the current value; users wishing to continue using the IonReader at the current - * depth will need to call {@link IonReader#next()} again. - * - * - If the IonReader is not positioned over a value (for example: because it is at the beginning or end of a - * stream), then this function does nothing. - * - If the current value is a container, this function will visit all of its child values and write those too, - * advancing the IonReader to the end of the container in the process. - * - If both this writer and the IonReader are in a struct, the writer will write the current value's field name. - * - If the writer is not in a struct but the reader is, the writer will ignore the current value's field name. - * - If the writer is in a struct but the IonReader is not, this function throws an IllegalStateException. - * - * @param reader The IonReader that will provide a value to write. - * @throws IOException if either the provided IonReader or this writer's underlying OutputStream throw an - * IOException. - * @throws IllegalStateException if this writer is inside a struct but the IonReader is not. - */ - final void writeValueRecursively(IonReader reader) throws IOException - { - // The IonReader does not need to be at the top level (getDepth()==0) when the function is called. - // We take note of its initial depth so we can avoid advancing the IonReader beyond the starting value. - int startingDepth = getDepth(); - - // The IonReader will be at `startingDepth` when the function is first called and then again when we - // have finished traversing all of its children. This boolean tracks which of those two states we are - // in when `getDepth() == startingDepth`. - boolean alreadyProcessedTheStartingValue = false; - - // The IonType of the IonReader's current value. - IonType type; - - while (true) { - // Each time we reach the top of the loop we are in one of three states: - // 1. We have not yet begun processing the starting value. - // 2. We are currently traversing the starting value's children. - // 3. We have finished processing the starting value. - if (getDepth() == startingDepth) { - // The IonReader is at the starting depth. We're either beginning our traversal or finishing it. - if (alreadyProcessedTheStartingValue) { - // We're finishing our traversal. - break; - } - // We're beginning our traversal. Don't advance the cursor; instead, use the current - // value's IonType. - type = reader.getType(); - // We've begun processing the starting value. - alreadyProcessedTheStartingValue = true; - } else { - // We're traversing the starting value's children (that is: values at greater depths). We need to - // advance the cursor by calling next(). - type = reader.next(); - } - - if (type == null) { - // There are no more values at this level. If we're at the starting level, we're done. - if (getDepth() == startingDepth) { - break; - } - // Otherwise, step out once and then try to move forward again. - reader.stepOut(); - stepOut(); - continue; - } - - // We found a value. Write out its field name and annotations, if any. - write_value_field_name_helper(reader); - write_value_annotations_helper(reader); - - if (reader.isNullValue()) { - this.writeNull(type); - continue; - } - - switch (type) { - case NULL: - // The isNullValue() check above will handle this. - throw new IllegalStateException("isNullValue() was false but IonType was NULL."); - case BOOL: - writeBool(reader.booleanValue()); - break; - case INT: - writeInt(reader.bigIntegerValue()); - break; - case FLOAT: - writeFloat(reader.doubleValue()); - break; - case DECIMAL: - writeDecimal(reader.decimalValue()); - break; - case TIMESTAMP: - writeTimestamp(reader.timestampValue()); - break; - case STRING: - writeString(reader.stringValue()); - break; - case SYMBOL: - writeSymbolToken(reader.symbolValue()); - break; - case BLOB: - writeBlob(reader.newBytes()); - break; - case CLOB: - writeClob(reader.newBytes()); - break; - case STRUCT: // Intentional fallthrough - case LIST: // Intentional fallthrough - case SEXP: - reader.stepIn(); - stepIn(type); - break; - default: - throw new IllegalStateException("Unknown value type: " + type); - } - } + DefaultReaderToWriterTransfer.writeValue(reader, this); } // diff --git a/src/main/java/com/amazon/ion/impl/bin/IonBinaryWriterAdapter.java b/src/main/java/com/amazon/ion/impl/bin/IonBinaryWriterAdapter.java index 3a5f3e9ab..2e1cce947 100644 --- a/src/main/java/com/amazon/ion/impl/bin/IonBinaryWriterAdapter.java +++ b/src/main/java/com/amazon/ion/impl/bin/IonBinaryWriterAdapter.java @@ -1,18 +1,5 @@ -/* - * 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. - */ - +// 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.IonBinaryWriter; @@ -133,6 +120,10 @@ public void setFieldNameSymbol(SymbolToken name) delegate.setFieldNameSymbol(name); } + public boolean isFieldNameSet() { + return delegate.isFieldNameSet(); + } + public void setTypeAnnotations(String... annotations) { delegate.setTypeAnnotations(annotations); @@ -158,6 +149,10 @@ public void stepOut() throws IOException delegate.stepOut(); } + public int getDepth() { + return delegate.getDepth(); + } + public boolean isInStruct() { return delegate.isInStruct(); diff --git a/src/main/java/com/amazon/ion/impl/bin/IonManagedWriter_1_1.kt b/src/main/java/com/amazon/ion/impl/bin/IonManagedWriter_1_1.kt index dcffab174..5fc7e1755 100644 --- a/src/main/java/com/amazon/ion/impl/bin/IonManagedWriter_1_1.kt +++ b/src/main/java/com/amazon/ion/impl/bin/IonManagedWriter_1_1.kt @@ -4,6 +4,7 @@ package com.amazon.ion.impl.bin import com.amazon.ion.* import com.amazon.ion.SymbolTable.* +import com.amazon.ion.eexp.* import com.amazon.ion.impl.* import com.amazon.ion.impl._Private_IonWriter.* import com.amazon.ion.impl.bin.LengthPrefixStrategy.* @@ -27,7 +28,7 @@ import java.util.* * * See also [ManagedWriterOptions_1_1], [SymbolInliningStrategy], and [LengthPrefixStrategy]. */ -internal class IonManagedWriter_1_1( +class IonManagedWriter_1_1 private constructor( private val userData: IonRawWriter_1_1, private val systemData: PrivateIonRawWriter_1_1, private val options: ManagedWriterOptions_1_1, @@ -872,8 +873,17 @@ internal class IonManagedWriter_1_1( } } - override fun writeObject(obj: WriteAsIon) { - obj.writeToMacroAware(this) + override fun writeObject(objekt: WriteAsIon) { + val builder = DirectEExpressionBuilder(this) + val eExpression = objekt.writeWithEExpression(builder) + if (eExpression == null) { + objekt.writeTo(this) + } else if (eExpression == DirectEExpression) { + // It was already written as the builder methods were being invoked. + } else { + eExpression as PreparedEExpression + eExpression.writeWithEExpression(builder) + } } /** diff --git a/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt b/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt index 36fe0b268..6b5ffebcb 100644 --- a/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt +++ b/src/main/java/com/amazon/ion/impl/bin/IonRawBinaryWriter_1_1.kt @@ -3,6 +3,7 @@ package com.amazon.ion.impl.bin import com.amazon.ion.* +import com.amazon.ion.eexp.* import com.amazon.ion.impl.* import com.amazon.ion.impl.bin.IonEncoder_1_1.* import com.amazon.ion.impl.bin.IonRawBinaryWriter_1_1.ContainerType.* @@ -607,6 +608,12 @@ class IonRawBinaryWriter_1_1 internal constructor( override fun writeClob(value: ByteArray, start: Int, length: Int) = writeScalar { writeClobValue(buffer, value, start, length) } + fun writeTaglessArgumentBytes(action: WriteBuffer.() -> Int) { + val bytesWritten = buffer.action() + currentContainer.length += bytesWritten + currentContainer.numChildren++ + } + override fun stepInList(usingLengthPrefix: Boolean) { openValue { currentContainer = containerStack.push { it.reset(LIST, buffer.position(), usingLengthPrefix) } diff --git a/src/main/java/com/amazon/ion/impl/bin/WriteBuffer.java b/src/main/java/com/amazon/ion/impl/bin/WriteBuffer.java index 2bb73772c..a1fed0aa1 100644 --- a/src/main/java/com/amazon/ion/impl/bin/WriteBuffer.java +++ b/src/main/java/com/amazon/ion/impl/bin/WriteBuffer.java @@ -1460,6 +1460,7 @@ public int writeFixedIntOrUInt(final long value, final int numBytes) { * either one as long as we provide the correct number of bytes needed to encode the value. */ private int _writeFixedIntOrUInt(final long value, final int numBytes) { + // TODO: Rewrite this using switch with fallthrough writeByte((byte) value); if (numBytes > 1) { writeByte((byte) (value >> 8)); diff --git a/src/main/java/com/amazon/ion/impl/macro/Macro.kt b/src/main/java/com/amazon/ion/impl/macro/Macro.kt index 7d6bf2266..0ae99ff31 100644 --- a/src/main/java/com/amazon/ion/impl/macro/Macro.kt +++ b/src/main/java/com/amazon/ion/impl/macro/Macro.kt @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 package com.amazon.ion.impl.macro +import com.amazon.ion.eexp.* import com.amazon.ion.impl.TaglessEncoding /** @@ -12,6 +13,9 @@ sealed interface Macro { val body: List? val dependencies: Iterable + fun createInvocation(): EExpressionArgumentBuilder = PreparedEExpressionArgumentBuilder(null, this) + fun createInvocation(name: String): EExpressionArgumentBuilder = PreparedEExpressionArgumentBuilder(name, this) + data class Parameter(val variableName: String, val type: ParameterEncoding, val cardinality: ParameterCardinality) { override fun toString() = "$type::$variableName${cardinality.sigil}" } @@ -33,6 +37,7 @@ sealed interface Macro { Float16("float16", TaglessEncoding.FLOAT16), Float32("float32", TaglessEncoding.FLOAT32), Float64("float64", TaglessEncoding.FLOAT64), + FlexString("flex_string", TaglessEncoding.FLEX_STRING), FlexSym("flex_sym", TaglessEncoding.FLEX_SYM), ; companion object { @@ -51,6 +56,7 @@ sealed interface Macro { TaglessEncoding.FLOAT16 -> Float16 TaglessEncoding.FLOAT32 -> Float32 TaglessEncoding.FLOAT64 -> Float64 + TaglessEncoding.FLEX_STRING -> FlexString TaglessEncoding.FLEX_SYM -> FlexSym } } diff --git a/src/test/java/com/amazon/ion/impl/bin/IonManagedWriter_1_1_Test.kt b/src/test/java/com/amazon/ion/impl/bin/IonManagedWriter_1_1_Test.kt index 5c6d776c0..acb9dac7a 100644 --- a/src/test/java/com/amazon/ion/impl/bin/IonManagedWriter_1_1_Test.kt +++ b/src/test/java/com/amazon/ion/impl/bin/IonManagedWriter_1_1_Test.kt @@ -4,6 +4,7 @@ package com.amazon.ion.impl.bin import com.amazon.ion.* import com.amazon.ion.IonEncodingVersion.* +import com.amazon.ion.eexp.* import com.amazon.ion.impl.* import com.amazon.ion.impl.macro.* import com.amazon.ion.impl.macro.ExpressionBuilderDsl.Companion.templateBody @@ -51,7 +52,12 @@ internal class IonManagedWriter_1_1_Test { .withSymbolInliningStrategy(symbolInliningStrategy) .apply { if (pretty) withPrettyPrinting() } .build(appendable) as IonManagedWriter_1_1 - writer.apply(block) + try { + writer.apply(block) + } catch (e: Exception) { + println(appendable.toString().trim()) + throw e + } if (closeWriter) writer.close() return appendable.toString().trim() } @@ -116,6 +122,26 @@ internal class IonManagedWriter_1_1_Test { ) } + @Test + fun `write a struct with a list field`() { + val expected = """ + $ion_1_1 + {foo:[1,2]} + """.trimIndent() + + val actual = write { + stepIn(IonType.STRUCT) + setFieldName("foo") + stepIn(IonType.LIST) + writeInt(1) + writeInt(2) + stepOut() + stepOut() + } + + assertEquals(expected, actual) + } + private fun newSystemReader(input: ByteArray): IonReader { val system = IonSystemBuilder.standard().build() as _Private_IonSystem return system.newSystemReader(input) @@ -911,10 +937,11 @@ internal class IonManagedWriter_1_1_Test { @Test fun `writeObject() should write something with nested macro representation`() { + // TODO: See if we can get rid of the expression group around "Blue". val expected = """ $ion_1_1 (:$ion::set_macros (:: (macro null (x*) (%x)) (macro Polygon (vertices+ flex_sym::fill?) {vertices:[(%vertices)],fill:(.0 (%fill))}) (macro Point2D (x y) {x:(%x),y:(%y)}))) - (:Polygon (:: (:Point2D 0 0) (:Point2D 0 1) (:Point2D 1 1) (:Point2D 1 0)) Blue) + (:Polygon (:: (:Point2D 0 0) (:Point2D 0 1) (:Point2D 1 1) (:Point2D 1 0)) (:: Blue)) """.trimIndent() val data = Polygon( @@ -978,15 +1005,18 @@ internal class IonManagedWriter_1_1_Test { } } - override fun writeToMacroAware(writer: MacroAwareIonWriter) { - with(writer) { - startMacro(MACRO_NAME, MACRO) - startExpressionGroup() - vertices.forEach { writer.writeObject(it) } - endExpressionGroup() - fill?.let { writeObject(it) } - endMacro() - } + override fun writeWithEExpression(builder: EExpressionBuilder): EExpression? { + return builder + .withName(MACRO_NAME) + .withMacro(MACRO) + .withArgument { w -> + vertices.forEachIndexed { i, it -> + println("$i; depth=${w.depth}") + w.writeObject(it) + } + } + .withArgument { w -> fill?.let { w.writeObject(it) } } + .build() } } @@ -1011,13 +1041,12 @@ internal class IonManagedWriter_1_1_Test { ) } - override fun writeToMacroAware(writer: MacroAwareIonWriter) { - with(writer) { - startMacro(MACRO_NAME, MACRO) - writeInt(x) - writeInt(y) - endMacro() - } + override fun writeWithEExpression(builder: EExpressionBuilder): EExpression? { + return builder.withName(MACRO_NAME) + .withMacro(MACRO) + .withIntArgument(x) + .withIntArgument(y) + .build() } override fun writeTo(writer: IonWriter) {