diff --git a/bosk-core/src/main/java/works/bosk/Bosk.java b/bosk-core/src/main/java/works/bosk/Bosk.java index fbe89002..c69869f0 100644 --- a/bosk-core/src/main/java/works/bosk/Bosk.java +++ b/bosk-core/src/main/java/works/bosk/Bosk.java @@ -29,6 +29,7 @@ import works.bosk.exceptions.InvalidTypeException; import works.bosk.exceptions.NoReadContextException; import works.bosk.exceptions.NonexistentReferenceException; +import works.bosk.exceptions.NotYetImplementedException; import works.bosk.exceptions.ReferenceBindingException; import works.bosk.util.Classes; @@ -79,6 +80,7 @@ public class Bosk implements BoskInfo { @Getter private final Identifier instanceID = Identifier.from(randomUUID().toString()); @Getter private final BoskDriver driver; @Getter private final BoskDiagnosticContext diagnosticContext = new BoskDiagnosticContext(); + private final BoskDriver userSuppliedDriver; private final LocalDriver localDriver; private final RootRef rootRef; private final ThreadLocal rootSnapshot = new ThreadLocal<>(); @@ -97,7 +99,7 @@ public class Bosk implements BoskInfo { * other state. * @param driverFactory Will be applied to this Bosk's local driver during * the Bosk's constructor, and the resulting {@link BoskDriver} will be the - * one returned by {@link #driver}. + * one returned by {@link #getDriver}. * * @see DriverStack */ @@ -124,7 +126,8 @@ public Bosk(String name, Type rootType, DefaultRootFunction defaultRootFuncti // to do such things as create References, so it needs the rest of the // initialization to have completed already. // - this.driver = driverFactory.build(boskInfo, this.localDriver); + this.userSuppliedDriver = driverFactory.build(boskInfo, this.localDriver); + this.driver = new ValidatingDriver(userSuppliedDriver); try { this.currentRoot = requireNonNull(driver.initialRoot(rootType)); @@ -168,6 +171,90 @@ public static BoskDriver simpleDriver(@SuppressWa return downstream; } + /** + * Evolution note: we need better handling of the driver stack. + * For now, we just provide access to the topmost driver, but code should be able + * to look up any driver on the stack. We need to think carefully about how we + * want this to work. + * + * @return the driver from the driver stack having the given type. + * @throws IllegalArgumentException if there is no unique driver of the given type + */ + @SuppressWarnings("unchecked") + public > D getDriver(Class driverType) { + if (driverType.isInstance(userSuppliedDriver)) { + return (D)driverType.cast(userSuppliedDriver); + } else { + throw new NotYetImplementedException("Can't look up driver of type " + driverType); + } + } + + /** + * We wrap the user-supplied driver with one of these to ensure the error-checking + * requirements of the {@link BoskDriver} are enforced. + */ + @RequiredArgsConstructor + final class ValidatingDriver implements BoskDriver { + final BoskDriver downstream; + + @Override + public void submitReplacement(Reference target, T newValue) { + assertCorrectBosk(target); + downstream.submitReplacement(target, newValue); + } + + @Override + public void submitConditionalReplacement(Reference target, T newValue, Reference precondition, Identifier requiredValue) { + assertCorrectBosk(target); + assertCorrectBosk(precondition); + downstream.submitConditionalReplacement(target, newValue, precondition, requiredValue); + } + + @Override + public void submitInitialization(Reference target, T newValue) { + assertCorrectBosk(target); + downstream.submitInitialization(target, newValue); + } + + @Override + public void submitDeletion(Reference target) { + if (target.path().isEmpty()) { + // TODO: Augment dereferencer so it can tell us this for all references, not just the root + throw new IllegalArgumentException("Cannot delete root object"); + } + assertCorrectBosk(target); + downstream.submitDeletion(target); + } + + @Override + public void submitConditionalDeletion(Reference target, Reference precondition, Identifier requiredValue) { + assertCorrectBosk(target); + assertCorrectBosk(precondition); + downstream.submitConditionalDeletion(target, precondition, requiredValue); + } + + @Override + public R initialRoot(Type rootType) throws InvalidTypeException, IOException, InterruptedException { + return downstream.initialRoot(rootType); + } + + @Override + public void flush() throws IOException, InterruptedException { + downstream.flush(); + } + + private void assertCorrectBosk(Reference target) { + // TODO: Do we need to be this strict? + // On the one hand, we could write conditional updates in a way that don't require the + // reference to point to the right bosk. + // On the other hand, there's a certain symmetry to requiring the references to have the right + // bosk for both reads and writes, and forcing this discipline on users might help them avoid + // some pretty confusing mistakes. + assert ((Bosk.RootRef)target.root()).bosk() == Bosk.this: "Reference supplied to driver operation must refer to the correct bosk"; + } + + } + /** * {@link BoskDriver} that writes directly to this {@link Bosk}. * @@ -210,7 +297,6 @@ public R initialRoot(Type rootType) throws InvalidTypeException { @Override public void submitReplacement(Reference target, T newValue) { - assertCorrectBosk(target); synchronized (this) { R priorRoot = currentRoot; if (!tryGraftReplacement(target, newValue)) { @@ -223,7 +309,6 @@ public void submitReplacement(Reference target, T newValue) { @Override public void submitInitialization(Reference target, T newValue) { - assertCorrectBosk(target); synchronized (this) { boolean preconditionsSatisfied; try (@SuppressWarnings("unused") ReadContext executionContext = supersedingReadContext()) { @@ -242,7 +327,6 @@ public void submitInitialization(Reference target, T newValue) { @Override public void submitDeletion(Reference target) { - assertCorrectBosk(target); synchronized (this) { R priorRoot = currentRoot; if (!tryGraftDeletion(target)) { @@ -261,8 +345,6 @@ public void flush() { @Override public void submitConditionalReplacement(Reference target, T newValue, Reference precondition, Identifier requiredValue) { - assertCorrectBosk(target); - assertCorrectBosk(precondition); synchronized (this) { boolean preconditionsSatisfied; try (@SuppressWarnings("unused") ReadContext executionContext = supersedingReadContext()) { @@ -281,8 +363,6 @@ public void submitConditionalReplacement(Reference target, T newValue, Re @Override public void submitConditionalDeletion(Reference target, Reference precondition, Identifier requiredValue) { - assertCorrectBosk(target); - assertCorrectBosk(precondition); synchronized (this) { boolean preconditionsSatisfied; try (@SuppressWarnings("unused") ReadContext executionContext = supersedingReadContext()) { @@ -309,16 +389,6 @@ void triggerEverywhere(HookRegistration reg) { drainQueueIfAllowed(); } - private void assertCorrectBosk(Reference target) { - // TODO: Do we need to be this strict? - // On the one hand, we could write conditional updates in a way that don't require the - // reference to point to the right bosk. - // On the other hand, there's a certain symmetry to requiring the references to have the right - // bosk for both reads and writes, and forcing this discipline on users might help them avoid - // some pretty confusing mistakes. - assert ((Bosk.RootRef)target.root()).bosk() == Bosk.this: "Reference supplied to driver operation must refer to the correct bosk"; - } - /** * @return false if the update was ignored */ @@ -348,9 +418,7 @@ private synchronized boolean tryGraftReplacement(Reference target, T newV */ private synchronized boolean tryGraftDeletion(Reference target) { Path targetPath = target.path(); - if (targetPath.isEmpty()) { - throw new IllegalArgumentException("Cannot delete root object"); - } + assert !targetPath.isEmpty(); Dereferencer dereferencer = dereferencerFor(target); try { LOGGER.debug("Applying deletion at {}", target); diff --git a/bosk-core/src/test/java/works/bosk/BoskConstructorTest.java b/bosk-core/src/test/java/works/bosk/BoskConstructorTest.java index c3c2c1c5..341a2b4a 100644 --- a/bosk-core/src/test/java/works/bosk/BoskConstructorTest.java +++ b/bosk-core/src/test/java/works/bosk/BoskConstructorTest.java @@ -48,7 +48,7 @@ void basicProperties_correctValues() { // The driver object and root node should be exactly the same object passed in - assertSame(driver.get(), bosk.driver()); + assertSame(driver.get(), bosk.getDriver(ForwardingDriver.class)); try (val __ = bosk.readContext()) { assertSame(root, bosk.rootReference().value()); diff --git a/bosk-core/src/test/java/works/bosk/BoskLocalReferenceTest.java b/bosk-core/src/test/java/works/bosk/BoskLocalReferenceTest.java index e9a4fbb6..317cc51c 100644 --- a/bosk-core/src/test/java/works/bosk/BoskLocalReferenceTest.java +++ b/bosk-core/src/test/java/works/bosk/BoskLocalReferenceTest.java @@ -8,7 +8,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.UnaryOperator; import lombok.AccessLevel; import lombok.EqualsAndHashCode; @@ -277,18 +276,6 @@ public InvalidRoot(Identifier id, Catalog entities, String str) { assertThrows(IllegalArgumentException.class, () -> new Bosk<>(boskName(), String.class, new InvalidRoot(Identifier.unique("yucky"), Catalog.empty(), "hello"), Bosk::simpleDriver)); } - @Test - void testDriver() { - // This doesn't test the operation of the driver; merely that the right driver is returned - AtomicReference> driver = new AtomicReference<>(); - Bosk myBosk = new Bosk<>(boskName(), Root.class, new Root(123, Catalog.empty()), (b,d) -> { - BoskDriver bd = new ProxyDriver(d); - driver.set(bd); - return bd; - }); - assertSame(driver.get(), myBosk.driver()); - } - @RequiredArgsConstructor private static final class ProxyDriver implements BoskDriver { @Delegate final BoskDriver delegate; diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverRecoveryTest.java b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverRecoveryTest.java index abf152c4..99f6b179 100644 --- a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverRecoveryTest.java +++ b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverRecoveryTest.java @@ -252,12 +252,17 @@ private void setRevision(long revisionNumber) { private TestEntity initializeDatabase(String distinctiveString) { try { + AtomicReference> driverRef = new AtomicReference<>(); Bosk prepBosk = new Bosk( boskName("Prep " + getClass().getSimpleName()), TestEntity.class, bosk -> initialRoot(bosk).withString(distinctiveString), - driverFactory); - MongoDriver driver = (MongoDriver) prepBosk.driver(); + (b,d) -> { + var mongoDriver = (MongoDriver) driverFactory.build(b, d); + driverRef.set(mongoDriver); + return mongoDriver; + }); + var driver = driverRef.get(); waitFor(driver); driver.close(); diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverSpecialTest.java b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverSpecialTest.java index bcae8830..8963be39 100644 --- a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverSpecialTest.java +++ b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/MongoDriverSpecialTest.java @@ -563,7 +563,7 @@ void refurbish_createsField() throws IOException, InterruptedException { assertEquals(Optional.empty(), before); // Not there yet LOGGER.debug("Call refurbish"); - ((MongoDriver)upgradeableBosk.driver()).refurbish(); + upgradeableBosk.getDriver(MongoDriver.class).refurbish(); originalBosk.driver().flush(); // Not the bosk that did refurbish! LOGGER.debug("Check state after"); @@ -621,7 +621,7 @@ void refurbish_fixesMetadata() throws IOException, InterruptedException { ); // (Close this so it doesn't crash when we delete the "path" field) - ((MongoDriver)initialBosk.driver()).close(); + initialBosk.getDriver(MongoDriver.class).close(); // Delete some metadata fields MongoCollection collection = mongoService.client() @@ -655,7 +655,7 @@ void refurbish_fixesMetadata() throws IOException, InterruptedException { } // Refurbish - ((MongoDriver)bosk.driver()).refurbish(); + bosk.getDriver(MongoDriver.class).refurbish(); // Verify the fields are all now there try (MongoCursor cursor = collection.find(filterDoc).cursor()) { diff --git a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/SchemaEvolutionTest.java b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/SchemaEvolutionTest.java index 82759249..8623c736 100644 --- a/bosk-mongo/src/test/java/works/bosk/drivers/mongo/SchemaEvolutionTest.java +++ b/bosk-mongo/src/test/java/works/bosk/drivers/mongo/SchemaEvolutionTest.java @@ -91,7 +91,8 @@ void pairwise_readCompatible() throws Exception { } LOGGER.debug("Refurbish"); - ((MongoDriver)toBosk.driver()).refurbish(); + MongoDriver driver = toBosk.getDriver(MongoDriver.class); + driver.refurbish(); LOGGER.debug("Perform fromBosk read"); try (var __ = fromBosk.readContext()) { @@ -117,7 +118,8 @@ void pairwise_writeCompatible() throws Exception { Refs toRefs = toBosk.buildReferences(Refs.class); LOGGER.debug("Refurbish toBosk ({})", toBosk.name()); - ((MongoDriver)toBosk.driver()).refurbish(); + MongoDriver driver = toBosk.getDriver(MongoDriver.class); + driver.refurbish(); flushIfLiveRefurbishIsNotSupported(fromBosk, fromHelper, toHelper);