From 16ee77b06983fa5eddc2993d61b016dc7d0444f3 Mon Sep 17 00:00:00 2001
From: Patrick Doyle
Date: Sat, 18 Jan 2025 21:54:18 -0500
Subject: [PATCH 1/3] Refactor: settings.gradle includes on separate lines
---
settings.gradle | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/settings.gradle b/settings.gradle
index 2f60ba18..0fd8d717 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,11 @@
rootProject.name = 'bosk'
-include 'bosk-annotations', 'bosk-core', 'bosk-jackson', 'bosk-logback', 'bosk-mongo', 'bosk-spring-boot-3', 'bosk-testing', 'lib-testing', 'example-hello'
+include 'bosk-annotations'
+include 'bosk-core'
+include 'bosk-jackson'
+include 'bosk-logback'
+include 'bosk-mongo'
+include 'bosk-spring-boot-3'
+include 'bosk-testing'
+include 'example-hello'
+include 'lib-testing'
From 7d800029ebcfde1f123e3e6fa4ad11ccf8d0c1e5 Mon Sep 17 00:00:00 2001
From: Patrick Doyle
Date: Sun, 19 Jan 2025 10:06:00 -0500
Subject: [PATCH 2/3] Refactor: rename assertRoundTripWorks
---
.../bosk/drivers/mongo/BsonSurgeonTest.java | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/BsonSurgeonTest.java b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/BsonSurgeonTest.java
index 31edc2a8..41a44e71 100644
--- a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/BsonSurgeonTest.java
+++ b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/BsonSurgeonTest.java
@@ -78,34 +78,34 @@ void setup() throws InvalidTypeException, IOException, InterruptedException {
@Test
void root_roundTripWorks() {
- doTest(bosk.rootReference());
+ assertRoundTripWorks(bosk.rootReference());
}
@Test
void catalog_roundTripWorks() {
- doTest(refs.catalog());
+ assertRoundTripWorks(refs.catalog());
}
@Test
void catalogEntry_roundTripWorks() {
- doTest(refs.entity(Identifier.from("entity1")));
+ assertRoundTripWorks(refs.entity(Identifier.from("entity1")));
}
@Test
void nestedCatalog_roundTripWorks() {
- doTest(refs.nestedCatalog(Identifier.from("entity1")));
+ assertRoundTripWorks(refs.nestedCatalog(Identifier.from("entity1")));
}
@Test
void childEntry_roundTripWorks() {
- doTest(refs.child(
+ assertRoundTripWorks(refs.child(
Identifier.from("entity1"),
Identifier.from("child1")));
}
@Test
void grandchildEntry_roundTripWorks() {
- doTest(refs.grandchild(
+ assertRoundTripWorks(refs.grandchild(
Identifier.from("entity1"),
Identifier.from("child1"),
Identifier.from("child1")));
@@ -113,7 +113,7 @@ void grandchildEntry_roundTripWorks() {
@Test
void sideTable_roundTripWorks() {
- doTest(refs.sideTable());
+ assertRoundTripWorks(refs.sideTable());
}
@Test
@@ -188,7 +188,7 @@ void duplicatePaths_throws() {
});
}
- private void doTest(Reference> mainRef) {
+ private void assertRoundTripWorks(Reference> mainRef) {
BsonDocument entireDoc;
try (var _ = bosk.readContext()) {
entireDoc = (BsonDocument) formatter.object2bsonValue(mainRef.value(), mainRef.targetType());
From 45684ff118796304d042181e6f7453ed7f6365b5 Mon Sep 17 00:00:00 2001
From: Patrick Doyle
Date: Sat, 18 Jan 2025 21:56:14 -0500
Subject: [PATCH 3/3] Split BSON functionality into its own library.
BSON turns out to be a convenient mutable representation of state trees.
It can be used outside the context of MongoDB.
---
bosk-bson/README.md | 7 +
bosk-bson/build.gradle | 47 +++
.../works/bosk/bson}/BsonFormatException.java | 2 +-
.../java/works/bosk/bson/BsonFormatter.java | 332 ++++++++++++++++++
.../java/works/bosk/bson}/BsonPlugin.java | 7 +-
.../java/works/bosk/bson}/BsonSurgeon.java | 60 ++--
.../works/bosk/bson/BsonFormatterTest.java | 51 +--
.../java/works/bosk/bson}/BsonPluginTest.java | 2 +-
.../works/bosk/bson}/BsonSurgeonTest.java | 19 +-
.../works/bosk/bson/DottedFieldNameTest.java | 10 +-
.../works/bosk/bson/DottedFieldNameTest2.java | 44 +++
bosk-core/src/main/java/works/bosk/Path.java | 3 +
.../src/main/java/works/bosk/Reference.java | 1 +
bosk-mongo/build.gradle | 1 +
.../works/bosk/drivers/mongo/Formatter.java | 255 +-------------
.../works/bosk/drivers/mongo/MainDriver.java | 3 +-
.../works/bosk/drivers/mongo/MongoDriver.java | 1 +
.../bosk/drivers/mongo/PandoFormatDriver.java | 46 +--
.../drivers/mongo/SequoiaFormatDriver.java | 5 +-
.../mongo/AbstractMongoDriverTest.java | 1 +
.../drivers/mongo/MiscMongoDriverTest.java | 14 +
.../mongo/MongoDriverConformanceTest.java | 1 +
.../drivers/mongo/MongoDriverHanoiTest.java | 1 +
.../drivers/mongo/MongoDriverSpecialTest.java | 1 +
.../drivers/mongo/SchemaEvolutionTest.java | 3 +
.../drivers/mongo/example/ExampleBosk.java | 2 +-
lib-testing/build.gradle | 2 +-
.../works/bosk/AbstractRoundTripTest.java | 3 +-
settings.gradle | 1 +
29 files changed, 549 insertions(+), 376 deletions(-)
create mode 100644 bosk-bson/README.md
create mode 100644 bosk-bson/build.gradle
rename {bosk-mongo/src/main/java/works/bosk/drivers/mongo => bosk-bson/src/main/java/works/bosk/bson}/BsonFormatException.java (88%)
create mode 100644 bosk-bson/src/main/java/works/bosk/bson/BsonFormatter.java
rename {bosk-mongo/src/main/java/works/bosk/drivers/mongo => bosk-bson/src/main/java/works/bosk/bson}/BsonPlugin.java (99%)
rename {bosk-mongo/src/main/java/works/bosk/drivers/mongo => bosk-bson/src/main/java/works/bosk/bson}/BsonSurgeon.java (83%)
rename bosk-mongo/src/test/java/works/bosk/drivers/mongo/FormatterTest.java => bosk-bson/src/test/java/works/bosk/bson/BsonFormatterTest.java (64%)
rename {bosk-mongo/src/test/java/works/bosk/drivers/mongo => bosk-bson/src/test/java/works/bosk/bson}/BsonPluginTest.java (98%)
rename {bosk-mongo/src/test/java/works/bosk/drivers/mongo => bosk-bson/src/test/java/works/bosk/bson}/BsonSurgeonTest.java (94%)
rename bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverDottedFieldNameTest.java => bosk-bson/src/test/java/works/bosk/bson/DottedFieldNameTest.java (92%)
create mode 100644 bosk-bson/src/test/java/works/bosk/bson/DottedFieldNameTest2.java
create mode 100644 bosk-mongo/src/test/java/works/bosk/drivers/mongo/MiscMongoDriverTest.java
diff --git a/bosk-bson/README.md b/bosk-bson/README.md
new file mode 100644
index 00000000..5e4e4529
--- /dev/null
+++ b/bosk-bson/README.md
@@ -0,0 +1,7 @@
+## bosk-bson
+
+This is the subproject for the published `bosk-bson` library,
+containing facilities for manipulating MongoDB's binary JSON data structures.
+Outside of MongoDB itself, we also use BSON as a handy utility
+for divide up and re-combine large JSON documents,
+which is useful even outside of MongoDB.
diff --git a/bosk-bson/build.gradle b/bosk-bson/build.gradle
new file mode 100644
index 00000000..648ba9df
--- /dev/null
+++ b/bosk-bson/build.gradle
@@ -0,0 +1,47 @@
+
+plugins {
+ id 'bosk.development'
+ id 'bosk.maven-publish'
+ id 'info.solidsoft.pitest' version '1.15.0'
+ id 'com.github.spotbugs' version '5.1.5'
+}
+
+base {
+ archivesName = 'bosk-bson'
+}
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(jdkVersion)
+ }
+}
+
+compileJava {
+ options.release = prodJavaVersion
+}
+
+compileTestJava {
+ options.release = null
+}
+
+dependencies {
+ api project(":bosk-core")
+ api 'org.mongodb:bson:5.1.2'
+ implementation 'com.github.spotbugs:spotbugs-annotations:4.8.6' // To stop warnings about When from MongoDB driver
+
+ testImplementation project(":bosk-logback")
+ testImplementation project(":bosk-testing")
+ testImplementation project(":lib-testing")
+}
+
+pitest {
+ pitestVersion = '1.15.0'
+ junit5PluginVersion = '1.2.0'
+ jvmArgs = ['-ea'] // Our unit tests check for assert statements
+ targetClasses = ['works.bosk.drivers.mongo.BsonSurgeon']
+ targetTests = ['works.bosk.drivers.mongo.BsonSurgeonTest']
+ threads = 4
+ outputFormats = ['XML', 'HTML']
+ timestampedReports = false
+ //verbose = true
+}
diff --git a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonFormatException.java b/bosk-bson/src/main/java/works/bosk/bson/BsonFormatException.java
similarity index 88%
rename from bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonFormatException.java
rename to bosk-bson/src/main/java/works/bosk/bson/BsonFormatException.java
index 6b3ecde4..7de567ef 100644
--- a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonFormatException.java
+++ b/bosk-bson/src/main/java/works/bosk/bson/BsonFormatException.java
@@ -1,4 +1,4 @@
-package works.bosk.drivers.mongo;
+package works.bosk.bson;
class BsonFormatException extends IllegalStateException {
public BsonFormatException(String s) { super(s); }
diff --git a/bosk-bson/src/main/java/works/bosk/bson/BsonFormatter.java b/bosk-bson/src/main/java/works/bosk/bson/BsonFormatter.java
new file mode 100644
index 00000000..9b36c529
--- /dev/null
+++ b/bosk-bson/src/main/java/works/bosk/bson/BsonFormatter.java
@@ -0,0 +1,332 @@
+package works.bosk.bson;
+
+import java.lang.reflect.Type;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+import java.util.regex.Pattern;
+import org.bson.BsonDocument;
+import org.bson.BsonDocumentWriter;
+import org.bson.BsonReader;
+import org.bson.BsonValue;
+import org.bson.codecs.Codec;
+import org.bson.codecs.DecoderContext;
+import org.bson.codecs.DocumentCodecProvider;
+import org.bson.codecs.EncoderContext;
+import org.bson.codecs.ValueCodecProvider;
+import org.bson.codecs.configuration.CodecRegistries;
+import org.bson.codecs.configuration.CodecRegistry;
+import works.bosk.BoskDiagnosticContext;
+import works.bosk.BoskInfo;
+import works.bosk.Listing;
+import works.bosk.Path;
+import works.bosk.Reference;
+import works.bosk.SerializationPlugin;
+import works.bosk.SideTable;
+import works.bosk.exceptions.InvalidTypeException;
+
+import static works.bosk.Path.validSegment;
+import static works.bosk.ReferenceUtils.rawClass;
+
+/**
+ * Utility class for encoding and decoding bosk state trees as BSON.
+ */
+public class BsonFormatter {
+ protected static final UnaryOperator DECODER;
+ protected static final UnaryOperator ENCODER;
+ protected final CodecRegistry simpleCodecs;
+ protected final Function> preferredBoskCodecs;
+ protected final Function, SerializationPlugin.DeserializationScope> deserializationScopeFunction;
+
+ public BsonFormatter(BoskInfo> boskInfo, BsonPlugin bsonPlugin) {
+ this.simpleCodecs = CodecRegistries.fromProviders(
+ bsonPlugin.codecProviderFor(boskInfo),
+ new ValueCodecProvider(),
+ new DocumentCodecProvider());
+ this.preferredBoskCodecs = type -> bsonPlugin.getCodec(type, rawClass(type), simpleCodecs, boskInfo);
+ this.deserializationScopeFunction = bsonPlugin::newDeserializationScope;
+ }
+
+ public static String dottedFieldNameSegment(String segment) {
+ return ENCODER.apply(validSegment(segment));
+ }
+
+ public static String undottedFieldNameSegment(String dottedSegment) {
+ return DECODER.apply(dottedSegment);
+ }
+
+ /**
+ * @param refLength behave as though ref were truncated to this length without actually having to do it
+ */
+ private static void buildDottedFieldNameOf(Reference ref, int startLength, int refLength, ArrayList segments) {
+ if (ref.path().length() > startLength) {
+ Reference> enclosingReference = ref.enclosingReference(Object.class);
+ BsonFormatter.buildDottedFieldNameOf(enclosingReference, startLength, refLength, segments);
+ if (ref.path().length() <= refLength) {
+ if (Listing.class.isAssignableFrom(enclosingReference.targetClass())) {
+ segments.add("ids");
+ } else if (SideTable.class.isAssignableFrom(enclosingReference.targetClass())) {
+ segments.add("valuesById");
+ }
+ segments.add(BsonFormatter.dottedFieldNameSegment(ref.path().lastSegment()));
+ }
+ }
+ }
+
+ static {
+ DECODER = s->{
+ return URLDecoder.decode(s, StandardCharsets.UTF_8);
+ };
+
+ ENCODER = s->{
+ // Selective percent-encoding of characters MongoDB doesn't like.
+ // Standard percent-encoding doesn't handle the period character, which
+ // we want, so if we're already diverging from the standard, we might
+ // as well do something that suits our needs.
+ // Good to stay compatible with standard percent-DEcoding, though.
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < s.length(); ) {
+ int cp = s.codePointAt(i);
+ switch (cp) {
+ case '%': // For percent-encoding
+ case '+': case ' ': // These two are affected by URLDecoder
+ case '$': // MongoDB treats these specially
+ case '.': // MongoDB separator for dotted field names
+ case 0: // Can MongoDB handle nulls? Probably. Do we want to find out? Not really.
+ case '|': // (These are reserved for internal use)
+ case '!':
+ case '~':
+ case '[':
+ case ']':
+ appendPercentEncoded(sb, cp);
+ break;
+ default:
+ sb.appendCodePoint(cp);
+ break;
+ }
+ i += Character.charCount(cp);
+ }
+ return sb.toString();
+ };
+ }
+
+ private static void appendPercentEncoded(StringBuilder sb, int cp) {
+ assert 0 <= cp && cp <= 255;
+ sb
+ .append('%')
+ .append(hexCharForDigit(cp / 16))
+ .append(hexCharForDigit(cp % 16));
+ }
+
+ /**
+ * An uppercase version of {@link Character#forDigit} with a radix of 16.
+ */
+ private static char hexCharForDigit(int value) {
+ if (value < 10) {
+ return (char)('0' + value);
+ } else {
+ return (char)('A' + value - 10);
+ }
+ }
+
+ /**
+ * @param ref the bosk node whose field name is required
+ * @param startingRef the bosk node corresponding to the MongoDB document in which {@code ref} is located
+ * @return MongoDB field name corresponding to {@code ref} within the document for {@code startingRef}, starting with {@link DocumentFields#state state}
+ * @see #referenceTo(String, Reference)
+ */
+ public static String dottedFieldNameOf(Reference ref, Reference> startingRef) {
+ assert startingRef.encloses(ref);
+ return dottedFieldNameOf(ref, ref.path().length(), startingRef);
+ }
+
+ /**
+ * @param refLength behave as though ref were truncated to this length, without actually having to do it
+ * @return MongoDB field name corresponding to the given Reference
+ * @see #referenceTo(String, Reference)
+ */
+ static String dottedFieldNameOf(Reference ref, int refLength, Reference> startingRef) {
+ // TODO: Is this required? It's currently only really called by tests
+ ArrayList segments = dottedFieldNameSegments(ref, refLength, startingRef);
+ return String.join(".", segments.toArray(new String[0]));
+ }
+
+ /**
+ * @return Reference corresponding to the given field name within a document representing {@code startingReference}
+ * @see #dottedFieldNameOf
+ */
+ @SuppressWarnings("unchecked")
+ public static Reference referenceTo(String dottedName, Reference> startingReference) throws InvalidTypeException {
+ Reference> ref = startingReference;
+ Iterator iter = Arrays.asList(dottedName.split(Pattern.quote("."))).iterator();
+ BsonFormatter.skipField(ref, iter, DocumentFields.state.name()); // The entire Bosk state is in this field
+ while (iter.hasNext()) {
+ if (Listing.class.isAssignableFrom(ref.targetClass())) {
+ BsonFormatter.skipField(ref, iter, "ids");
+ } else if (SideTable.class.isAssignableFrom(ref.targetClass())) {
+ BsonFormatter.skipField(ref, iter, "valuesById");
+ }
+ if (iter.hasNext()) {
+ String segment = undottedFieldNameSegment(iter.next());
+ ref = ref.then(Object.class, segment);
+ }
+ }
+ return (Reference) ref;
+ }
+
+ private static void skipField(Reference> ref, Iterator iter, String expectedName) {
+ String actualName;
+ try {
+ actualName = iter.next();
+ } catch (NoSuchElementException e) {
+ throw new IllegalStateException("Expected '" + expectedName + "' for " + ref.targetClass().getSimpleName() + "; encountered end of dotted field name");
+ }
+ if (!expectedName.equals(actualName)) {
+ throw new IllegalStateException("Expected '" + expectedName + "' for " + ref.targetClass().getSimpleName() + "; was: " + actualName);
+ }
+ }
+
+ /**
+ * A BSON path is a pipe-delimited sequence of BSON fields.
+ *
+ *
+ * Note that the BSON structure is such that the BSON path's segments may
+ * not exactly match those of {@code ref.path()}.
+ *
+ *
+ * This computes the BSON path for {@code ref} within a document representing {@code startingRef}.
+ *
+ * @param ref reference to the node whose path is desired
+ * @param startingRef reference to the node corresponding to the document containing the {@code ref} node
+ * @return the BSON path leading to {@code ref} within a document representing {@code startingRef}
+ */
+ public static String docBsonPath(Reference> ref, Reference> startingRef) {
+ return "|" + String.join("|", docSegments(ref, startingRef));
+ }
+
+ /**
+ * @return list of field names suitable for {@link #lookup} to find the document corresponding
+ * to docRef inside a document corresponding to rootRef
+ */
+ private static List docSegments(Reference> docRef, Reference> rootRef) {
+ ArrayList allSegments = dottedFieldNameSegments(docRef, docRef.path().length(), rootRef);
+ return allSegments
+ .subList(1, allSegments.size()); // Skip the "state" field
+ }
+
+ protected Codec> codecFor(Type type) {
+ // BsonPlugin gives better codecs than CodecRegistry, because BsonPlugin is aware of generics,
+ // so we always try that first. The CodecSupplier protocol uses "null" to indicate that another
+ // CodecSupplier should be used, so we follow that protocol and fall back on the CodecRegistry.
+ // TODO: Should this logic be in BsonPlugin? It has nothing to do with MongoDriver really.
+ Codec> result = preferredBoskCodecs.apply(type);
+ if (result == null) {
+ return simpleCodecs.get(rawClass(type));
+ } else {
+ return result;
+ }
+ }
+
+ /**
+ * Converts a bosk state node to BSON.
+ *
+ *
+ * A common way to call this is, for a given {@link Reference} {@code ref}, is:
+ *
+ * ref.value(), ref.targetType()
+ *
+ * @param object the bosk state node to convert
+ * @param type the type of {@code object}
+ * @see #bsonValue2object(BsonValue, Reference)
+ */
+ @SuppressWarnings("unchecked")
+ public BsonValue object2bsonValue(T object, Type type) {
+ rawClass(type).cast(object);
+ Codec objectCodec = (Codec) codecFor(type);
+ BsonDocument document = new BsonDocument();
+ try (BsonDocumentWriter writer = new BsonDocumentWriter(document)) {
+ // To support arbitrary values, not just whole documents, we put the result INSIDE a document.
+ writer.writeStartDocument();
+ writer.writeName("value");
+ objectCodec.encode(writer, object, EncoderContext.builder().build());
+ writer.writeEndDocument();
+ }
+ return document.get("value");
+ }
+
+ /**
+ * Converts a BSON value to a bosk state node.
+ * @param bson the value to convert
+ * @param target the bosk location of the resulting node
+ * @see #object2bsonValue(Object, Type)
+ */
+ @SuppressWarnings("unchecked")
+ public T bsonValue2object(BsonValue bson, Reference target) {
+ Codec objectCodec = (Codec) codecFor(target.targetType());
+ BsonDocument document = new BsonDocument();
+ document.append("value", bson);
+ try (
+ @SuppressWarnings("unused") SerializationPlugin.DeserializationScope scope = deserializationScopeFunction.apply(target);
+ BsonReader reader = document.asBsonReader()
+ ) {
+ reader.readStartDocument();
+ reader.readName("value");
+ return objectCodec.decode(reader, DecoderContext.builder().build());
+ }
+ }
+
+ /**
+ * The fields of the main MongoDB document. Case-sensitive.
+ *
+ *
+ * No field name should be a prefix of any other.
+ */
+ public enum DocumentFields {
+ /**
+ * The location of this document's state within the conceptual giant document.
+ * A pipe-separated list of BSON field names.
+ */
+ path,
+
+ /**
+ * The BSON-encoded portion of the bosk state represented by this document.
+ */
+ state,
+
+ /**
+ * An ever-increasing 64-bit long that is incremented every time the document changes.
+ */
+ revision,
+
+ /**
+ * The contents of {@link BoskDiagnosticContext} corresponding to this
+ * document's last update.
+ */
+ diagnostics,
+ }
+
+ static ArrayList dottedFieldNameSegments(Reference ref, int refLength, Reference> startingRef) {
+ assert startingRef.path().matchesPrefixOf(ref.path()): "'" + ref + "' must be under '" + startingRef + "'";
+ ArrayList segments = new ArrayList<>();
+ segments.add(DocumentFields.state.name());
+ buildDottedFieldNameOf(ref, startingRef.path().length(), refLength, segments);
+ return segments;
+ }
+
+ /**
+ * @param elementRefLength behave as though elementRef were truncated to this length, without actually having to do it
+ * @return MongoDB field name corresponding to the object that contains the given element
+ * @see #referenceTo(String, Reference)
+ */
+ static List containerSegments(Reference elementRef, int elementRefLength, Reference> startingRef) {
+ List elementSegments = dottedFieldNameSegments(elementRef, elementRefLength, startingRef);
+ return elementSegments.subList(0, elementSegments.size()-1); // Trim off the element itself
+ }
+
+}
diff --git a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonPlugin.java b/bosk-bson/src/main/java/works/bosk/bson/BsonPlugin.java
similarity index 99%
rename from bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonPlugin.java
rename to bosk-bson/src/main/java/works/bosk/bson/BsonPlugin.java
index 17dbed7f..87c9afa8 100644
--- a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonPlugin.java
+++ b/bosk-bson/src/main/java/works/bosk/bson/BsonPlugin.java
@@ -1,11 +1,10 @@
-package works.bosk.drivers.mongo;
+package works.bosk.bson;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.RecordComponent;
import java.lang.reflect.Type;
@@ -66,8 +65,8 @@
import static works.bosk.ReferenceUtils.getterMethod;
import static works.bosk.ReferenceUtils.parameterType;
import static works.bosk.ReferenceUtils.rawClass;
-import static works.bosk.drivers.mongo.Formatter.dottedFieldNameSegment;
-import static works.bosk.drivers.mongo.Formatter.undottedFieldNameSegment;
+import static works.bosk.bson.BsonFormatter.dottedFieldNameSegment;
+import static works.bosk.bson.BsonFormatter.undottedFieldNameSegment;
public final class BsonPlugin extends SerializationPlugin {
private final ValueCodecProvider valueCodecProvider = new ValueCodecProvider();
diff --git a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonSurgeon.java b/bosk-bson/src/main/java/works/bosk/bson/BsonSurgeon.java
similarity index 83%
rename from bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonSurgeon.java
rename to bosk-bson/src/main/java/works/bosk/bson/BsonSurgeon.java
index 3fa14d1a..eb9a5b71 100644
--- a/bosk-mongo/src/main/java/works/bosk/drivers/mongo/BsonSurgeon.java
+++ b/bosk-bson/src/main/java/works/bosk/bson/BsonSurgeon.java
@@ -1,4 +1,4 @@
-package works.bosk.drivers.mongo;
+package works.bosk.bson;
import java.util.ArrayList;
import java.util.Arrays;
@@ -11,27 +11,32 @@
import org.bson.BsonInvalidOperationException;
import org.bson.BsonString;
import org.bson.BsonValue;
-import works.bosk.Bosk;
import works.bosk.EnumerableByIdentifier;
import works.bosk.Identifier;
import works.bosk.Path;
import works.bosk.Reference;
-import works.bosk.SideTable;
+import works.bosk.bson.BsonFormatter.DocumentFields;
import works.bosk.exceptions.InvalidTypeException;
import static java.util.Collections.emptyList;
+import static java.util.Collections.unmodifiableList;
import static java.util.Comparator.comparing;
import static java.util.Objects.requireNonNull;
-import static works.bosk.drivers.mongo.Formatter.containerSegments;
-import static works.bosk.drivers.mongo.Formatter.dottedFieldNameSegments;
-import static works.bosk.drivers.mongo.Formatter.undottedFieldNameSegment;
+import static works.bosk.bson.BsonFormatter.containerSegments;
+import static works.bosk.bson.BsonFormatter.undottedFieldNameSegment;
/**
* Splits up a single large BSON document into multiple self-describing pieces,
* and re-assembles them. Provides the core mechanism to carve large BSON structures
* into pieces so they can stay under the MongoDB document size limit.
+ *
*
+ * Beware: {@link BsonSurgeon} takes advantage of the mutability of the
+ * {@link BsonDocument} data structure to perform surgeries efficiently.
+ * As a consequence, the {@link #scatter} and {@link #gather} methods
+ * will modify objects passed in as arguments.
*
+ *
* Jargon:
*
*
Root document
@@ -40,9 +45,13 @@
*
BSON document corresponding to the part of the state tree being described
*
*/
-class BsonSurgeon {
+public class BsonSurgeon {
final List graftPoints;
+ /**
+ * Internal representation of a graft point, storing a pre-built placeholder
+ * reference for an entry in the grafted container for efficiency.
+ */
record GraftPoint (
Reference extends EnumerableByIdentifier>> containerRef,
Reference> entryPlaceholderRef
@@ -64,9 +73,9 @@ public GraftPoint boundTo(Identifier id) {
*/
private static final String BSON_PATH_FIELD = "_id";
- private static final String STATE_FIELD = Formatter.DocumentFields.state.name();
+ private static final String STATE_FIELD = DocumentFields.state.name();
- BsonSurgeon(List>> graftPoints) {
+ public BsonSurgeon(List>> graftPoints) {
this.graftPoints = new ArrayList<>(graftPoints.size());
graftPoints.stream()
// Scatter bottom-up so we don't have to worry about scattering already-scattered documents
@@ -75,7 +84,7 @@ public GraftPoint boundTo(Identifier id) {
GraftPoint.of(containerRef, entryRef(containerRef))));
}
- static Reference> entryRef(Reference extends EnumerableByIdentifier>> containerRef) {
+ private static Reference> entryRef(Reference extends EnumerableByIdentifier>> containerRef) {
// We need a reference pointing all the way to the collection entry, so that if the
// collection itself has BSON fields (like SideTable does), those fields will be included
// in the dotted name segment list. The actual ID we pick doesn't matter and will be ignored.
@@ -94,34 +103,23 @@ static Reference> entryRef(Reference extends EnumerableByIdentifier>> cont
*
* @param docRef the bosk node corresponding to document
* @param document will be modified!
- * @param rootRef {@link Bosk#rootReference()}
* @return list of {@link BsonDocument}s which, when passed to {@link #gather}, combine to form the original document.
* The main document, document corresponding to docRef, will be at the end of the list.
* @see #gather
*/
- public List scatter(Reference> docRef, BsonDocument document, Reference> rootRef) {
+ public List scatter(Reference> docRef, BsonDocument document) {
List parts = new ArrayList<>();
for (GraftPoint graftPoint: graftPoints) {
- scatterOneCollection(docRef, graftPoint, document, rootRef, parts);
+ scatterOneCollection(docRef, graftPoint, document, docRef.root(), parts);
}
- // `document` has now had the scattered pieces replaced by BsonBoolean.TRUE
- String docBsonPath = "|" + String.join("|", docSegments(docRef, rootRef));
- parts.add(createRecipe(document, docBsonPath));
+ // `document` has now had the scattered pieces stubbed-out by BsonBoolean.TRUE.
+ // Add the stubbed-out document as the final recipe in the parts list.
+ parts.add(createRecipe(document, BsonFormatter.docBsonPath(docRef, docRef.root())));
return parts;
}
- /**
- * @return list of field names suitable for {@link #lookup} to find the document corresponding
- * to docRef inside a document corresponding to rootRef
- */
- static List docSegments(Reference> docRef, Reference> rootRef) {
- ArrayList allSegments = dottedFieldNameSegments(docRef, docRef.path().length(), rootRef);
- return allSegments
- .subList(1, allSegments.size()); // Skip the "state" field
- }
-
private void scatterOneCollection(Reference> docRef, GraftPoint graftPoint, BsonDocument docToScatter, Reference> rootRef, List parts) {
// Only continue if the graft could point to a proper descendant node of docRef
Path graftPath = graftPoint.entryPlaceholderRef.path();
@@ -164,11 +162,6 @@ private static List containerDocSegments(Reference> docRef, int docRef
return allSegments.subList(1, allSegments.size()); // Remove "state" segment
}
- /**
- * entryPath and entryBsonPath must correspond to each other.
- * They'll have the same segments, except where the BSON representation of a container actually contains its own
- * fields (as with {@link SideTable}, in which case those fields will appear too.
- */
private static BsonDocument createRecipe(BsonValue entryState, String bsonPathString) {
return new BsonDocument()
.append(BSON_PATH_FIELD, new BsonString(bsonPathString))
@@ -204,6 +197,11 @@ private static BsonDocument lookup(BsonDocument entireDoc, List segments
*
*
* partsList is a list of "instructions" for assembling a larger document.
+ * Each part contains a {@link DocumentFields#path path} field containing a {@link BsonFormatter#docBsonPath BSON path}
+ * that indicates where that part fits into the larger document;
+ * and a {@link DocumentFields#state state} field with the contents of the part.
+ *
+ *