diff --git a/java/pom.xml b/java/pom.xml index f7f5c42..559e337 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -18,12 +18,31 @@ Licensed under the MIT License. https://github.com/Azure/azure-sdk-for-java + + azure-cosmos-serialization + default + dev-azure-com-azure-cosmos-java-azure-cosmos-serialization + https://pkgs.dev.azure.com/azure-cosmos-java/_packaging/azure-cosmos-serialization/maven/v1 + azure-java-build-docs ${site.url}/site/${project.artifactId} + + + dev-azure-com-azure-cosmos-java-azure-cosmos-serialization + https://pkgs.dev.azure.com/azure-cosmos-java/_packaging/azure-cosmos-serialization/maven/v1 + + true + + + true + + + + https://github.com/Azure/azure-sdk-for-java diff --git a/java/src/main/java/com/azure/data/cosmos/core/Out.java b/java/src/main/java/com/azure/data/cosmos/core/Out.java index da839c1..ecca988 100644 --- a/java/src/main/java/com/azure/data/cosmos/core/Out.java +++ b/java/src/main/java/com/azure/data/cosmos/core/Out.java @@ -59,7 +59,7 @@ public final class Out { return true; } - if (!(other instanceof Out)) { + if (other.getClass() != Out.class) { return false; } diff --git a/java/src/main/java/com/azure/data/cosmos/core/Reference.java b/java/src/main/java/com/azure/data/cosmos/core/Reference.java index d7c2506..4bdc68c 100644 --- a/java/src/main/java/com/azure/data/cosmos/core/Reference.java +++ b/java/src/main/java/com/azure/data/cosmos/core/Reference.java @@ -9,8 +9,8 @@ import java.util.Objects; * A container object which may or may not contain a non-null value. * * This is a value-based class and as such use of identity-sensitive operations--including reference equality - * ({@code ==}), identity hash code, or synchronization--on instances of {@Reference} may have unpredictable results - * and should be avoided. + * ({@code ==}), identity hash code, or synchronization--on instances of {@link Reference} may have unpredictable + * results and should be avoided. * * @param */ @@ -62,7 +62,7 @@ public final class Reference { return true; } - if (!(other instanceof Reference)) { + if (other.getClass() != Reference.class) { return false; } diff --git a/java/src/main/java/com/azure/data/cosmos/core/Utf8String.java b/java/src/main/java/com/azure/data/cosmos/core/Utf8String.java index ea30f65..16d89fe 100644 --- a/java/src/main/java/com/azure/data/cosmos/core/Utf8String.java +++ b/java/src/main/java/com/azure/data/cosmos/core/Utf8String.java @@ -465,6 +465,9 @@ public final class Utf8String implements ByteBufHolder, CharSequence, Comparable if (this.buffer == null) { return "null"; } + if (this.buffer.writerIndex() == 0) { + return "\"\""; + } return Json.toString(this.buffer.getCharSequence(0, this.buffer.writerIndex(), UTF_8)); } diff --git a/java/src/main/java/com/azure/data/cosmos/serialization/hybridrow/io/DataItem.java b/java/src/main/java/com/azure/data/cosmos/serialization/hybridrow/io/DataItem.java index 01474da..2c3c2ae 100644 --- a/java/src/main/java/com/azure/data/cosmos/serialization/hybridrow/io/DataItem.java +++ b/java/src/main/java/com/azure/data/cosmos/serialization/hybridrow/io/DataItem.java @@ -21,9 +21,6 @@ import static com.google.common.base.Preconditions.checkNotNull; */ public class DataItem { - private final Supplier fullPath; - private final Supplier name; - @JsonProperty private final List nodes; @@ -33,6 +30,9 @@ public class DataItem { @JsonProperty private final Object value; + private final Supplier name; + private final Supplier path; + @SuppressWarnings("UnstableApiUsage") DataItem( @Nonnull final List nodes, @@ -56,7 +56,7 @@ public class DataItem { this.name = Suppliers.memoize(() -> this.nodes.get(this.nodes.size() - 1)); - this.fullPath = Suppliers.memoize(() -> { + this.path = Suppliers.memoize(() -> { if (this.nodes.size() == 1) { return this.nodes.get(0); @@ -69,8 +69,11 @@ public class DataItem { int i; - for (i = 0; i < this.nodes.size() - 1; i++) { + for (i = 0; i < this.nodes.size() - 1; ++i) { builder.append(this.nodes.get(i)); + if (this.nodes.get(i + 1).charAt(0) != '[') { + builder.append('.'); + } } return builder.append(this.nodes.get(i)).toString(); @@ -86,7 +89,7 @@ public class DataItem { } public String path() { - return this.fullPath.get(); + return this.path.get(); } @Override diff --git a/java/src/main/java/com/azure/data/cosmos/serialization/hybridrow/io/RowIterable.java b/java/src/main/java/com/azure/data/cosmos/serialization/hybridrow/io/RowIterable.java new file mode 100644 index 0000000..76ecd69 --- /dev/null +++ b/java/src/main/java/com/azure/data/cosmos/serialization/hybridrow/io/RowIterable.java @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.data.cosmos.serialization.hybridrow.io; + +import com.azure.data.cosmos.core.Out; +import com.azure.data.cosmos.core.Utf8String; +import com.azure.data.cosmos.serialization.hybridrow.HybridRowVersion; +import com.azure.data.cosmos.serialization.hybridrow.Result; +import com.azure.data.cosmos.serialization.hybridrow.RowBuffer; +import com.azure.data.cosmos.serialization.hybridrow.layouts.LayoutResolver; +import com.azure.data.cosmos.serialization.hybridrow.layouts.LayoutResolverNamespace; +import com.azure.data.cosmos.serialization.hybridrow.layouts.LayoutType; +import com.azure.data.cosmos.serialization.hybridrow.schemas.Namespace; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Stack; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Strings.lenientFormat; +import static java.util.Objects.requireNonNull; + +public class RowIterable implements AutoCloseable, Iterable { + + private AtomicBoolean closed; + private final ByteBuf data; + private final LayoutResolver resolver; + + private RowIterable(LayoutResolver resolver, ByteBuf data) { + this.closed = new AtomicBoolean(); + this.data = data; + this.resolver = resolver; + } + + @Override + public void close() { + if (this.closed.compareAndSet(false, true)) { + this.data.release(); + } + } + + @Nonnull + @Override + public Iterator iterator() { + + checkState(!this.closed.get(), "RowIterable is closed"); + + final RowBuffer buffer = new RowBuffer(this.data, HybridRowVersion.V1, this.resolver); + final RowReader reader = new RowReader(buffer); + + return new RowIterator(reader); + } + + public static RowIterable open(@Nonnull Namespace namespace, @Nonnull File file) throws IOException { + + checkNotNull(file, "expected non-null file"); + + final long length = file.length(); + checkArgument(0 < length, "file does not exist: %s", file); + checkArgument(length <= Integer.MAX_VALUE, "expected file length <= %s, not %s", Integer.MAX_VALUE, length); + + ByteBuf data = Unpooled.buffer((int) length); + + try (InputStream stream = Files.newInputStream(file.toPath())) { + data.writeBytes(stream, (int) length); + } + + LayoutResolverNamespace resolver = new LayoutResolverNamespace(namespace); + return new RowIterable(resolver, data); + } + + public static RowIterable open(@Nonnull Namespace namespace, @Nonnull Path path) throws IOException { + return RowIterable.open(namespace, requireNonNull(path, "expected non-null path").toFile()); + } + + public static RowIterable open(@Nonnull Namespace namespace, @Nonnull String path) throws IOException { + return RowIterable.open(namespace, new File(requireNonNull(path, "expected non-null path"))); + } + + private static class RowIterator implements Iterator { + + final Stack paths; + final Stack readers; + final Out value; + + DataItem dataItem; + RowReader reader; + + RowIterator(RowReader reader) { + this.readers = new Stack<>(); + this.paths = new Stack<>(); + this.reader = reader; + this.value = new Out(); + } + + @Override + public boolean hasNext() { + + while (this.dataItem == null) { + if (this.reader == null) { + return false; + } + this.advance(); + } + return true; + } + + /** + * Returns the next element in the iteration. + * + * @return the next element in the iteration + * @throws NoSuchElementException if the iteration has no more elements + */ + @Override + public DataItem next() { + + while (this.dataItem == null) { + if (this.reader == null) { + throw new NoSuchElementException(); + } + this.advance(); + } + + DataItem dataItem = this.dataItem; + this.dataItem = null; + + return dataItem; + } + + @SuppressWarnings("unchecked") + private void advance() { + + do { + while (this.reader.read()) { + + final Result result; + + Utf8String path = this.reader.path(); + checkState(!path.isNull(), "expected non-null value for path"); + + LayoutType type = this.reader.type(); + checkState(type != null, "expected non-null type"); + + switch (type.layoutCode()) { + + case BOOLEAN: { + result = this.reader.readBoolean(this.value); + break; + } + case INT_16: { + result = this.reader.readInt16(this.value); + break; + } + case INT_32: { + result = this.reader.readInt32(this.value); + break; + } + case INT_64: { + result = this.reader.readInt64(this.value); + break; + } + case UINT_8: { + result = this.reader.readUInt8(this.value); + break; + } + case UINT_32: { + result = this.reader.readUInt32(this.value); + break; + } + case UINT_64: { + result = this.reader.readUInt64(this.value); + break; + } + case BINARY: { + result = this.reader.readBinary(this.value); + break; + } + case GUID: { + result = this.reader.readGuid(this.value); + break; + } + case NULL: + case BOOLEAN_FALSE: + case INT_8: + case UINT_16: + case VAR_INT: + case VAR_UINT: + case FLOAT_32: + case FLOAT_64: + case FLOAT_128: + case DECIMAL: + case DATE_TIME: + case UNIX_DATE_TIME: + case UTF_8: { + result = Result.SUCCESS; + break; + } + case NULLABLE_SCOPE: + case IMMUTABLE_NULLABLE_SCOPE: { + if (!this.reader.hasValue()) { + result = Result.SUCCESS; + break; + } + } + case ARRAY_SCOPE: + case IMMUTABLE_ARRAY_SCOPE: + + case MAP_SCOPE: + case IMMUTABLE_MAP_SCOPE: + + case OBJECT_SCOPE: + case IMMUTABLE_OBJECT_SCOPE: + + case SCHEMA: + case IMMUTABLE_SCHEMA: + + case SET_SCOPE: + case IMMUTABLE_SET_SCOPE: + + case TAGGED2_SCOPE: + case IMMUTABLE_TAGGED2_SCOPE: + + case TAGGED_SCOPE: + case IMMUTABLE_TAGGED_SCOPE: + + case TUPLE_SCOPE: + case IMMUTABLE_TUPLE_SCOPE: + + case TYPED_ARRAY_SCOPE: + case IMMUTABLE_TYPED_ARRAY_SCOPE: + + case TYPED_MAP_SCOPE: + case IMMUTABLE_TYPED_MAP_SCOPE: + + case TYPED_SET_SCOPE: + case IMMUTABLE_TYPED_SET_SCOPE: + + case TYPED_TUPLE_SCOPE: + case IMMUTABLE_TYPED_TUPLE_SCOPE: { + + this.readers.push(this.reader); + + this.paths.push(path.isEmpty() + ? Utf8String.transcodeUtf16(lenientFormat("[%s]", this.reader.index())) + : path); + + this.reader = this.reader.readScope(); + continue; + } + case MONGODB_OBJECT_ID: { + throw new IllegalStateException(lenientFormat("unsupported layout type: %s", type)); + } + case END_SCOPE: + case INVALID: { + throw new IllegalStateException(lenientFormat("unexpected layout type: %s", type)); + } + default: { + throw new IllegalStateException(lenientFormat("unknown layout type: %s", type)); + } + } + + if (result != Result.SUCCESS) { + String message = lenientFormat("failed to read %s value for %s", type.layoutCode(), path); + throw new IllegalStateException(message); + } + + this.dataItem = new DataItem(this.paths, path, type.layoutCode(), this.value.get()); + return; + } + + if (this.readers.empty()) { + this.reader = null; + } else { + this.reader = this.readers.pop(); + this.paths.pop(); + } + } + while (this.reader != null); + } + } +} diff --git a/java/src/main/java/com/azure/data/cosmos/serialization/hybridrow/io/RowScanner.java b/java/src/main/java/com/azure/data/cosmos/serialization/hybridrow/io/RowScanner.java index 3fdc7c4..1d54d0c 100644 --- a/java/src/main/java/com/azure/data/cosmos/serialization/hybridrow/io/RowScanner.java +++ b/java/src/main/java/com/azure/data/cosmos/serialization/hybridrow/io/RowScanner.java @@ -199,7 +199,10 @@ public class RowScanner implements AutoCloseable { case TYPED_TUPLE_SCOPE: case IMMUTABLE_TYPED_TUPLE_SCOPE: { - visitor.nodes().push(path); + visitor.nodes().push(path.isEmpty() + ? Utf8String.transcodeUtf16(lenientFormat("[%s]", reader.index())) + : path); + result = reader.readScope(visitor, RowScanner::visit); visitor.nodes().pop(); diff --git a/java/src/test/java/com/azure/data/cosmos/serialization/hybridrow/io/RowReaderTest.java b/java/src/test/java/com/azure/data/cosmos/serialization/hybridrow/io/RowReaderTest.java index ae74239..7a87499 100644 --- a/java/src/test/java/com/azure/data/cosmos/serialization/hybridrow/io/RowReaderTest.java +++ b/java/src/test/java/com/azure/data/cosmos/serialization/hybridrow/io/RowReaderTest.java @@ -20,16 +20,12 @@ import org.testng.annotations.Factory; import org.testng.annotations.Test; import org.testng.util.Strings; -import javax.annotation.Nonnull; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.Stack; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @@ -45,6 +41,8 @@ import static org.testng.AssertJUnit.assertNotNull; public class RowReaderTest { + // region Fields + private static final String basedir = System.getProperty("project.basedir", System.getProperty("user.dir")); private final Path dataFile; @@ -52,6 +50,10 @@ public class RowReaderTest { private Namespace namespace; + // endregion + + // region Construction and Setup + private RowReaderTest(File schemaFile, Path dataFile) { this.schemaFile = schemaFile; this.dataFile = dataFile; @@ -65,13 +67,15 @@ public class RowReaderTest { ); } + // endregion + @Test(groups = "unit") public void testIterable() throws IOException { - - final RowIterable iterable = RowIterable.of(this.namespace, this.dataFile); - - for (DataItem item : iterable) { - assertNotNull(item.toString()); + try (final RowIterable iterable = RowIterable.open(this.namespace, this.dataFile)) { + for (DataItem item : iterable) { + assertNotNull(item); + out.println(item); + } } } @@ -105,18 +109,19 @@ public class RowReaderTest { } @Test(groups = "unit") - public void testScanner() throws IOException { - - final RowScanner scanner = RowScanner.open(this.namespace, this.dataFile); - - scanner.visit((DataItem item, Object context) -> { + public void testScanner() throws Exception { + try (final RowScanner scanner = RowScanner.open(this.namespace, this.dataFile)) { + scanner.visit((DataItem item, Object context) -> { assertNull(context); assertNotNull(item); out.println(item); return Result.SUCCESS; }, null); + } } + // region Privates + @SuppressWarnings("unchecked") private static Result visitFields(RowReader reader, int level) { @@ -281,224 +286,5 @@ public class RowReaderTest { } } - public static class RowIterable implements Iterable { - - final ByteBuf data; - final LayoutResolver resolver; - - private RowIterable(LayoutResolver resolver, ByteBuf data) { - this.resolver = resolver; - this.data = data; - } - - @Nonnull - @Override - public Iterator iterator() { - final RowBuffer buffer = new RowBuffer(this.data, HybridRowVersion.V1, this.resolver); - final RowReader reader = new RowReader(buffer); - return new RowIterator(reader); - } - - public static RowIterable of(@Nonnull Namespace namespace, @Nonnull File file) throws IOException { - - checkNotNull(file, "expected non-null file"); - - final long length = file.length(); - checkArgument(0 < length, "file does not exist: %s", file); - checkArgument(length <= Integer.MAX_VALUE, "expected file length <= %s, not %s", Integer.MAX_VALUE, length); - - ByteBuf data = Unpooled.buffer((int) length); - - try (InputStream stream = Files.newInputStream(file.toPath())) { - data.writeBytes(stream, (int) length); - } - - LayoutResolverNamespace resolver = new LayoutResolverNamespace(namespace); - return new RowIterable(resolver, data); - } - - public static RowIterable of(@Nonnull Namespace namespace, @Nonnull Path path) throws IOException { - return RowIterable.of(namespace, requireNonNull(path, "expected non-null path").toFile()); - } - - public static RowIterable of(@Nonnull Namespace namespace, @Nonnull String path) throws IOException { - return RowIterable.of(namespace, new File(requireNonNull(path, "expected non-null path"))); - } - - private static class RowIterator implements Iterator { - - final Stack paths; - final Stack readers; - final Out value; - - RowReader reader; - Result result; - - RowIterator(RowReader reader) { - this.readers = new Stack<>(); - this.paths = new Stack<>(); - this.reader = reader; - this.value = new Out(); - } - - @Override - public boolean hasNext() { - return this.reader != null; - } - - /** - * Returns the next element in the iteration. - * - * @return the next element in the iteration - * @throws NoSuchElementException if the iteration has no more elements - */ - @SuppressWarnings("unchecked") - @Override - public DataItem next() { - - do { - while (this.reader.read()) { - - Utf8String path = this.reader.path(); - checkState(!path.isNull(), "expected non-null path"); - - LayoutType type = this.reader.type(); - checkState(type != null, "expected non-null type"); - - switch (type.layoutCode()) { - - case BOOLEAN: { - this.result = this.reader.readBoolean(this.value); - break; - } - case INT_16: { - this.result = this.reader.readInt16(this.value); - break; - } - case INT_32: { - this.result = this.reader.readInt32(this.value); - break; - } - case INT_64: { - this.result = this.reader.readInt64(this.value); - break; - } - case UINT_8: { - this.result = this.reader.readUInt8(this.value); - break; - } - case UINT_32: { - this.result = this.reader.readUInt32(this.value); - break; - } - case UINT_64: { - this.result = this.reader.readUInt64(this.value); - break; - } - case BINARY: { - this.result = this.reader.readBinary(this.value); - break; - } - case GUID: { - this.result = this.reader.readGuid(this.value); - break; - } - case NULL: - case BOOLEAN_FALSE: - case INT_8: - case UINT_16: - case VAR_INT: - case VAR_UINT: - case FLOAT_32: - case FLOAT_64: - case FLOAT_128: - case DECIMAL: - case DATE_TIME: - case UNIX_DATE_TIME: - case UTF_8: { - break; - } - case NULLABLE_SCOPE: - case IMMUTABLE_NULLABLE_SCOPE: { - if (!this.reader.hasValue()) { - break; - } - } - case ARRAY_SCOPE: - case IMMUTABLE_ARRAY_SCOPE: - - case MAP_SCOPE: - case IMMUTABLE_MAP_SCOPE: - - case OBJECT_SCOPE: - case IMMUTABLE_OBJECT_SCOPE: - - case SCHEMA: - case IMMUTABLE_SCHEMA: - - case SET_SCOPE: - case IMMUTABLE_SET_SCOPE: - - case TAGGED2_SCOPE: - case IMMUTABLE_TAGGED2_SCOPE: - - case TAGGED_SCOPE: - case IMMUTABLE_TAGGED_SCOPE: - - case TUPLE_SCOPE: - case IMMUTABLE_TUPLE_SCOPE: - - case TYPED_ARRAY_SCOPE: - case IMMUTABLE_TYPED_ARRAY_SCOPE: - - case TYPED_MAP_SCOPE: - case IMMUTABLE_TYPED_MAP_SCOPE: - - case TYPED_SET_SCOPE: - case IMMUTABLE_TYPED_SET_SCOPE: - - case TYPED_TUPLE_SCOPE: - case IMMUTABLE_TYPED_TUPLE_SCOPE: { - - this.readers.push(this.reader); - this.paths.push(path); - this.reader = this.reader.readScope(); - continue; - } - case END_SCOPE: { - final RowReader child = this.reader; - this.reader = this.readers.pop(); - continue; - } - case INVALID: - case MONGODB_OBJECT_ID: { - throw new IllegalStateException(lenientFormat("unsupported layout type: %s", type)); - } - default: { - throw new IllegalStateException(lenientFormat("unknown layout type: %s", type)); - } - } - - if (this.result != Result.SUCCESS) { - String message = lenientFormat("failed to read %s value for %s", type.layoutCode(), path); - throw new IllegalStateException(message); - } - - return new DataItem(this.paths, path, type.layoutCode(), this.value.get()); - } - - if (this.readers.empty()) { - this.reader = null; - } else { - this.reader = this.readers.pop(); - this.paths.pop(); - } - } - while (this.reader != null); - - throw new NoSuchElementException(); - } - } - } - + // endregion } \ No newline at end of file