From 40e3744b949a981f35cc3c119b6f59df4816f5aa Mon Sep 17 00:00:00 2001 From: Jeff Yemin Date: Fri, 19 Jul 2024 19:21:48 -0400 Subject: [PATCH 1/7] Support any number of Document Sequence Sections in CommandMessage#getCommandDocument JAVA-5536 --- .../connection/ByteBufBsonDocument.java | 40 ++++-------- .../internal/connection/CommandMessage.java | 63 +++++++++++++------ 2 files changed, 55 insertions(+), 48 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java b/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java index 5ab265c2bc8..19c5c8a40ee 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java +++ b/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java @@ -53,38 +53,26 @@ final class ByteBufBsonDocument extends BsonDocument { private final transient ByteBuf byteBuf; - static List createList(final ByteBufferBsonOutput bsonOutput, final int startPosition) { - List duplicateByteBuffers = bsonOutput.getByteBuffers(); - CompositeByteBuf outputByteBuf = new CompositeByteBuf(duplicateByteBuffers); - outputByteBuf.position(startPosition); + // Create a list of ByteBufBsonDocument from a ByteBuf positioned at the start of the first document of an OP_MSG Section + // of type Kind 1: Document Sequence + // The provided ByteBuf will be positioned at the end of the section upon normal completion of the method + static List createList(final ByteBuf outputByteBuf) { List documents = new ArrayList<>(); - int curDocumentStartPosition = startPosition; while (outputByteBuf.hasRemaining()) { - int documentSizeInBytes = outputByteBuf.getInt(); - ByteBuf slice = outputByteBuf.duplicate(); - slice.position(curDocumentStartPosition); - slice.limit(curDocumentStartPosition + documentSizeInBytes); - documents.add(new ByteBufBsonDocument(slice)); - curDocumentStartPosition += documentSizeInBytes; - outputByteBuf.position(outputByteBuf.position() + documentSizeInBytes - 4); - } - for (ByteBuf byteBuffer : duplicateByteBuffers) { - byteBuffer.release(); + ByteBufBsonDocument curDocument = createOne(outputByteBuf); + documents.add(curDocument); } return documents; } - static ByteBufBsonDocument createOne(final ByteBufferBsonOutput bsonOutput, final int startPosition) { - List duplicateByteBuffers = bsonOutput.getByteBuffers(); - CompositeByteBuf outputByteBuf = new CompositeByteBuf(duplicateByteBuffers); - outputByteBuf.position(startPosition); + // Create a ByteBufBsonDocument from a ByteBuf positioned at the start of a BSON document. + // The provided ByteBuf will be positioned at the end of the section upon normal completion of the method + static ByteBufBsonDocument createOne(final ByteBuf outputByteBuf) { int documentSizeInBytes = outputByteBuf.getInt(); ByteBuf slice = outputByteBuf.duplicate(); - slice.position(startPosition); - slice.limit(startPosition + documentSizeInBytes); - for (ByteBuf byteBuffer : duplicateByteBuffers) { - byteBuffer.release(); - } + slice.position(slice.position() - 4); + slice.limit(slice.position() + documentSizeInBytes); + outputByteBuf.position(outputByteBuf.position() + documentSizeInBytes - 4); return new ByteBufBsonDocument(slice); } @@ -138,10 +126,6 @@ T findInDocument(final Finder finder) { return finder.notFound(); } - int getSizeInBytes() { - return byteBuf.getInt(byteBuf.position()); - } - BsonDocument toBaseBsonDocument() { ByteBuf duplicateByteBuf = byteBuf.duplicate(); try (BsonBinaryReader bsonReader = new BsonBinaryReader(new ByteBufferBsonInput(duplicateByteBuf))) { diff --git a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java index 24b30d60acb..8efa6572128 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java +++ b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java @@ -30,12 +30,14 @@ import org.bson.BsonElement; import org.bson.BsonInt64; import org.bson.BsonString; +import org.bson.ByteBuf; import org.bson.FieldNameValidator; import org.bson.io.BsonOutput; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.stream.IntStream; import static com.mongodb.ReadPreference.primary; import static com.mongodb.ReadPreference.primaryPreferred; @@ -46,6 +48,8 @@ import static com.mongodb.connection.ServerType.SHARD_ROUTER; import static com.mongodb.connection.ServerType.STANDALONE; import static com.mongodb.internal.connection.BsonWriterHelper.writePayload; +import static com.mongodb.internal.connection.ByteBufBsonDocument.createList; +import static com.mongodb.internal.connection.ByteBufBsonDocument.createOne; import static com.mongodb.internal.connection.ReadConcernHelper.getReadConcernDocument; import static com.mongodb.internal.operation.ServerVersionHelper.FOUR_DOT_TWO_WIRE_VERSION; import static com.mongodb.internal.operation.ServerVersionHelper.FOUR_DOT_ZERO_WIRE_VERSION; @@ -107,30 +111,49 @@ public final class CommandMessage extends RequestMessage { this.serverApi = serverApi; } + // Create a BsonDocument representing the logical document encoded by an OP_MSG. + // The returned document will contain all the fields, including ones represented by + // OP_MSG Sections of type Kind 1: Document Sequence BsonDocument getCommandDocument(final ByteBufferBsonOutput bsonOutput) { - ByteBufBsonDocument byteBufBsonDocument = ByteBufBsonDocument.createOne(bsonOutput, - getEncodingMetadata().getFirstDocumentPosition()); - BsonDocument commandBsonDocument; - - if (containsPayload()) { - commandBsonDocument = byteBufBsonDocument.toBaseBsonDocument(); - - int payloadStartPosition = getEncodingMetadata().getFirstDocumentPosition() - + byteBufBsonDocument.getSizeInBytes() - + 1 // payload type - + 4 // payload size - + payload.getPayloadName().getBytes(StandardCharsets.UTF_8).length + 1; // null-terminated UTF-8 payload name - commandBsonDocument.append(payload.getPayloadName(), - new BsonArray(ByteBufBsonDocument.createList(bsonOutput, payloadStartPosition))); - } else { - commandBsonDocument = byteBufBsonDocument; + List duplicateByteBuffers = bsonOutput.getByteBuffers(); + try { + CompositeByteBuf outputByteBuf = new CompositeByteBuf(duplicateByteBuffers); + outputByteBuf.position(getEncodingMetadata().getFirstDocumentPosition()); + ByteBufBsonDocument byteBufBsonDocument = createOne(outputByteBuf); + + if (outputByteBuf.hasRemaining()) { + BsonDocument commandBsonDocument = byteBufBsonDocument.toBaseBsonDocument(); + + while (outputByteBuf.hasRemaining()) { + outputByteBuf.position(outputByteBuf.position() + 1 /* payload type */ + 4 /* payload size */); + String payloadName = getPayloadName(outputByteBuf); + assertFalse(payloadName.contains(".")); + commandBsonDocument.append(payloadName, new BsonArray(createList(outputByteBuf))); + } + return commandBsonDocument; + } else { + return byteBufBsonDocument; + } + } finally { + duplicateByteBuffers.forEach(ByteBuf::release); } - - return commandBsonDocument; } - boolean containsPayload() { - return payload != null; + // Get the field name from a ByteBuf positioned at the start of the document sequence identifier of an OP_MSG Section of type + // Kind 1: Document Sequence. + // Upon normal completion of the method, the ByteBuf will be positioned at the start of the first BSON object in the sequence. + private String getPayloadName(final ByteBuf outputByteBuf) { + List payloadNameBytes = new ArrayList<>(); + byte curByte = outputByteBuf.get(); + while (curByte != 0) { + payloadNameBytes.add(curByte); + curByte = outputByteBuf.get(); + } + + // Convert List to byte[] + byte[] byteArray = new byte[payloadNameBytes.size()]; + IntStream.range(0, payloadNameBytes.size()).forEachOrdered(i -> byteArray[i] = payloadNameBytes.get(i)); + return new String(byteArray, StandardCharsets.UTF_8); } boolean isResponseExpected() { From 09216456980822e23ba6bea277aec94030961ad0 Mon Sep 17 00:00:00 2001 From: Jeff Yemin Date: Sat, 20 Jul 2024 13:06:26 -0400 Subject: [PATCH 2/7] Add inline comments --- .../main/com/mongodb/internal/connection/CommandMessage.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java index 8efa6572128..8e4f9b390bb 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java +++ b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java @@ -121,9 +121,12 @@ BsonDocument getCommandDocument(final ByteBufferBsonOutput bsonOutput) { outputByteBuf.position(getEncodingMetadata().getFirstDocumentPosition()); ByteBufBsonDocument byteBufBsonDocument = createOne(outputByteBuf); + // If true, it means there is at least one Document Sequence in the OP_MSG if (outputByteBuf.hasRemaining()) { BsonDocument commandBsonDocument = byteBufBsonDocument.toBaseBsonDocument(); + // Each loop iteration processes one Document Sequence + // When there are no more bytes remaining, there are no more Document Sequences while (outputByteBuf.hasRemaining()) { outputByteBuf.position(outputByteBuf.position() + 1 /* payload type */ + 4 /* payload size */); String payloadName = getPayloadName(outputByteBuf); From 7870aed8c80ee7e81763afcb84920683cde079b1 Mon Sep 17 00:00:00 2001 From: Jeff Yemin Date: Sat, 20 Jul 2024 15:27:14 -0400 Subject: [PATCH 3/7] Style changes --- .../connection/ByteBufBsonDocument.java | 15 +++-- .../internal/connection/CommandMessage.java | 62 +++++++++++-------- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java b/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java index 19c5c8a40ee..7383eaa84b0 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java +++ b/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java @@ -53,9 +53,12 @@ final class ByteBufBsonDocument extends BsonDocument { private final transient ByteBuf byteBuf; - // Create a list of ByteBufBsonDocument from a ByteBuf positioned at the start of the first document of an OP_MSG Section - // of type Kind 1: Document Sequence - // The provided ByteBuf will be positioned at the end of the section upon normal completion of the method + /** + * Create a list of ByteBufBsonDocument from a buffer positioned at the start of the first document of an OP_MSG Section + * of type Document Sequence (Kind 1). + *

+ * The provided buffer will be positioned at the end of the section upon normal completion of the method + */ static List createList(final ByteBuf outputByteBuf) { List documents = new ArrayList<>(); while (outputByteBuf.hasRemaining()) { @@ -65,8 +68,10 @@ static List createList(final ByteBuf outputByteBuf) { return documents; } - // Create a ByteBufBsonDocument from a ByteBuf positioned at the start of a BSON document. - // The provided ByteBuf will be positioned at the end of the section upon normal completion of the method + /** + * Create a ByteBufBsonDocument from a buffer positioned at the start of a BSON document. + * The provided buffer will be positioned at the end of the document upon normal completion of the method + */ static ByteBufBsonDocument createOne(final ByteBuf outputByteBuf) { int documentSizeInBytes = outputByteBuf.getInt(); ByteBuf slice = outputByteBuf.duplicate(); diff --git a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java index 8e4f9b390bb..46eb9447a11 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java +++ b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java @@ -111,40 +111,50 @@ public final class CommandMessage extends RequestMessage { this.serverApi = serverApi; } - // Create a BsonDocument representing the logical document encoded by an OP_MSG. - // The returned document will contain all the fields, including ones represented by - // OP_MSG Sections of type Kind 1: Document Sequence + /** + * Create a BsonDocument representing the logical document encoded by an OP_MSG. + *

+ * The returned document will contain all the fields from the Body (Kind 0) Section, as well as all fields represented by + * OP_MSG Document Sequence (Kind 1) Sections. + */ BsonDocument getCommandDocument(final ByteBufferBsonOutput bsonOutput) { - List duplicateByteBuffers = bsonOutput.getByteBuffers(); + List byteBuffers = bsonOutput.getByteBuffers(); try { - CompositeByteBuf outputByteBuf = new CompositeByteBuf(duplicateByteBuffers); - outputByteBuf.position(getEncodingMetadata().getFirstDocumentPosition()); - ByteBufBsonDocument byteBufBsonDocument = createOne(outputByteBuf); - - // If true, it means there is at least one Document Sequence in the OP_MSG - if (outputByteBuf.hasRemaining()) { - BsonDocument commandBsonDocument = byteBufBsonDocument.toBaseBsonDocument(); - - // Each loop iteration processes one Document Sequence - // When there are no more bytes remaining, there are no more Document Sequences - while (outputByteBuf.hasRemaining()) { - outputByteBuf.position(outputByteBuf.position() + 1 /* payload type */ + 4 /* payload size */); - String payloadName = getPayloadName(outputByteBuf); - assertFalse(payloadName.contains(".")); - commandBsonDocument.append(payloadName, new BsonArray(createList(outputByteBuf))); + CompositeByteBuf byteBuf = new CompositeByteBuf(byteBuffers); + try { + byteBuf.position(getEncodingMetadata().getFirstDocumentPosition()); + ByteBufBsonDocument byteBufBsonDocument = createOne(byteBuf); + + // If true, it means there is at least one Kind 1:Document Sequence in the OP_MSG + if (byteBuf.hasRemaining()) { + BsonDocument commandBsonDocument = byteBufBsonDocument.toBaseBsonDocument(); + + // Each loop iteration processes one Document Sequence + // When there are no more bytes remaining, there are no more Document Sequences + while (byteBuf.hasRemaining()) { + byteBuf.position(byteBuf.position() + 1 /* payload type */ + 4 /* payload size */); + String payloadName = getPayloadName(byteBuf); + assertFalse(payloadName.contains(".")); + commandBsonDocument.append(payloadName, new BsonArray(createList(byteBuf))); + } + return commandBsonDocument; + } else { + return byteBufBsonDocument; } - return commandBsonDocument; - } else { - return byteBufBsonDocument; + } finally { + byteBuf.release(); } } finally { - duplicateByteBuffers.forEach(ByteBuf::release); + byteBuffers.forEach(ByteBuf::release); } } - // Get the field name from a ByteBuf positioned at the start of the document sequence identifier of an OP_MSG Section of type - // Kind 1: Document Sequence. - // Upon normal completion of the method, the ByteBuf will be positioned at the start of the first BSON object in the sequence. + /** + * Get the field name from a buffer positioned at the start of the document sequence identifier of an OP_MSG Section of type + * Document Sequence (Kind 1). + *

+ * Upon normal completion of the method, the buffer will be positioned at the start of the first BSON object in the sequence. + */ private String getPayloadName(final ByteBuf outputByteBuf) { List payloadNameBytes = new ArrayList<>(); byte curByte = outputByteBuf.get(); From c207fdfe409873e165791b425b70933b17c478bd Mon Sep 17 00:00:00 2001 From: Jeff Yemin Date: Thu, 25 Jul 2024 20:09:29 -0400 Subject: [PATCH 4/7] Refactor createOne --- .../mongodb/internal/connection/ByteBufBsonDocument.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java b/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java index 7383eaa84b0..70ed10a75a8 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java +++ b/driver-core/src/main/com/mongodb/internal/connection/ByteBufBsonDocument.java @@ -73,11 +73,11 @@ static List createList(final ByteBuf outputByteBuf) { * The provided buffer will be positioned at the end of the document upon normal completion of the method */ static ByteBufBsonDocument createOne(final ByteBuf outputByteBuf) { + int documentStart = outputByteBuf.position(); int documentSizeInBytes = outputByteBuf.getInt(); - ByteBuf slice = outputByteBuf.duplicate(); - slice.position(slice.position() - 4); - slice.limit(slice.position() + documentSizeInBytes); - outputByteBuf.position(outputByteBuf.position() + documentSizeInBytes - 4); + int documentEnd = documentStart + documentSizeInBytes; + ByteBuf slice = outputByteBuf.duplicate().position(documentStart).limit(documentEnd); + outputByteBuf.position(documentEnd); return new ByteBufBsonDocument(slice); } From 49bf6f8157b1727e5c40a0e0be22e8a17ee0a984 Mon Sep 17 00:00:00 2001 From: Jeff Yemin Date: Thu, 25 Jul 2024 20:19:08 -0400 Subject: [PATCH 5/7] Refactor sequence identifier creation. --- .../internal/connection/CommandMessage.java | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java index 46eb9447a11..f474f8b5bfc 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java +++ b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java @@ -17,6 +17,7 @@ package com.mongodb.internal.connection; import com.mongodb.MongoClientException; +import com.mongodb.MongoInternalException; import com.mongodb.MongoNamespace; import com.mongodb.ReadPreference; import com.mongodb.ServerApi; @@ -34,10 +35,11 @@ import org.bson.FieldNameValidator; import org.bson.io.BsonOutput; +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import java.util.stream.IntStream; import static com.mongodb.ReadPreference.primary; import static com.mongodb.ReadPreference.primaryPreferred; @@ -133,9 +135,11 @@ BsonDocument getCommandDocument(final ByteBufferBsonOutput bsonOutput) { // When there are no more bytes remaining, there are no more Document Sequences while (byteBuf.hasRemaining()) { byteBuf.position(byteBuf.position() + 1 /* payload type */ + 4 /* payload size */); - String payloadName = getPayloadName(byteBuf); - assertFalse(payloadName.contains(".")); - commandBsonDocument.append(payloadName, new BsonArray(createList(byteBuf))); + String fieldName = getSequenceIdentifier(byteBuf); + // If this assertion fires, it means that the driver has started using document sequences for nested fields. If + // so, the line following this assertion would have to change in order to handle that situation. + assertFalse(fieldName.contains(".")); + commandBsonDocument.append(fieldName, new BsonArray(createList(byteBuf))); } return commandBsonDocument; } else { @@ -155,18 +159,18 @@ BsonDocument getCommandDocument(final ByteBufferBsonOutput bsonOutput) { *

* Upon normal completion of the method, the buffer will be positioned at the start of the first BSON object in the sequence. */ - private String getPayloadName(final ByteBuf outputByteBuf) { - List payloadNameBytes = new ArrayList<>(); - byte curByte = outputByteBuf.get(); + private String getSequenceIdentifier(final ByteBuf byteBuf) { + ByteArrayOutputStream sequenceIdentifierBytes = new ByteArrayOutputStream(); + byte curByte = byteBuf.get(); while (curByte != 0) { - payloadNameBytes.add(curByte); - curByte = outputByteBuf.get(); + sequenceIdentifierBytes.write(curByte); + curByte = byteBuf.get(); + } + try { + return sequenceIdentifierBytes.toString(StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new MongoInternalException("Unexpected exception", e); } - - // Convert List to byte[] - byte[] byteArray = new byte[payloadNameBytes.size()]; - IntStream.range(0, payloadNameBytes.size()).forEachOrdered(i -> byteArray[i] = payloadNameBytes.get(i)); - return new String(byteArray, StandardCharsets.UTF_8); } boolean isResponseExpected() { From ec9b6bb6b227acc0c9aa470b6b2898cc66250841 Mon Sep 17 00:00:00 2001 From: Jeff Yemin Date: Thu, 25 Jul 2024 21:00:56 -0400 Subject: [PATCH 6/7] Fix horrible bug in CommandMessage#getCommandDocument --- .../internal/connection/CommandMessage.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java index f474f8b5bfc..3d17f88adfa 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java +++ b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java @@ -134,12 +134,24 @@ BsonDocument getCommandDocument(final ByteBufferBsonOutput bsonOutput) { // Each loop iteration processes one Document Sequence // When there are no more bytes remaining, there are no more Document Sequences while (byteBuf.hasRemaining()) { - byteBuf.position(byteBuf.position() + 1 /* payload type */ + 4 /* payload size */); + // skip reading the payload type, we know it is 1 + byteBuf.position(byteBuf.position() + 1); + int sequenceStart = byteBuf.position(); + int sequenceSizeInBytes = byteBuf.getInt(); + int sectionEnd = sequenceStart + sequenceSizeInBytes; + String fieldName = getSequenceIdentifier(byteBuf); // If this assertion fires, it means that the driver has started using document sequences for nested fields. If - // so, the line following this assertion would have to change in order to handle that situation. + // so, this method will need to change in order to append the value to the correct nested document. assertFalse(fieldName.contains(".")); - commandBsonDocument.append(fieldName, new BsonArray(createList(byteBuf))); + + ByteBuf documentsByteBufSlice = byteBuf.duplicate().limit(sectionEnd); + try { + commandBsonDocument.append(fieldName, new BsonArray(createList(documentsByteBufSlice))); + } finally { + documentsByteBufSlice.release(); + } + byteBuf.position(sectionEnd); } return commandBsonDocument; } else { From 74f73517bc7169fe4df6f2f639392e560ae7a6b8 Mon Sep 17 00:00:00 2001 From: Jeff Yemin Date: Thu, 25 Jul 2024 21:15:48 -0400 Subject: [PATCH 7/7] checkstyle --- .../main/com/mongodb/internal/connection/CommandMessage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java index 3d17f88adfa..901d7e9bb30 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java +++ b/driver-core/src/main/com/mongodb/internal/connection/CommandMessage.java @@ -139,7 +139,7 @@ BsonDocument getCommandDocument(final ByteBufferBsonOutput bsonOutput) { int sequenceStart = byteBuf.position(); int sequenceSizeInBytes = byteBuf.getInt(); int sectionEnd = sequenceStart + sequenceSizeInBytes; - + String fieldName = getSequenceIdentifier(byteBuf); // If this assertion fires, it means that the driver has started using document sequences for nested fields. If // so, this method will need to change in order to append the value to the correct nested document.