diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeMain.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeMain.java index 544debd7..da97aa03 100644 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeMain.java +++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeMain.java @@ -5,8 +5,8 @@ import uk.co.compendiumdev.challenge.apimodel.ChallengeApiModel; import uk.co.compendiumdev.challenge.challengers.Challengers; import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.http.ThingifierHttpApiRoutings; import uk.co.compendiumdev.thingifier.adapter.spark.MainImplementation; +import uk.co.compendiumdev.thingifier.adapter.spark.ThingifierHttpApiRoutings; import uk.co.compendiumdev.thingifier.core.repository.ThingStoreProviderConfig; public class ChallengeMain { diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeRouteHandler.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeRouteHandler.java index 1f092b0b..dd86ddb1 100644 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeRouteHandler.java +++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeRouteHandler.java @@ -10,7 +10,7 @@ import uk.co.compendiumdev.challenge.practicemodes.simpleapi.SimpleApiRoutes; import uk.co.compendiumdev.challenge.practicemodes.simulation.SimulationRoutes; import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.http.ThingifierHttpApiRoutings; +import uk.co.compendiumdev.thingifier.adapter.spark.ThingifierHttpApiRoutings; import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; import uk.co.compendiumdev.thingifier.htmlgui.htmlgen.DefaultGUIHTML; diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/AuthRoutes.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/AuthRoutes.java index a8e396a4..350009cd 100644 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/AuthRoutes.java +++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/AuthRoutes.java @@ -10,8 +10,9 @@ import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpRequest; import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.HttpApiResponseToInternalHttpResponse; import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.InternalHttpRequestToHttpApiRequest; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.InternalHttpResponseToSpark; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.SparkToInternalHttpRequest; +import uk.co.compendiumdev.thingifier.adapter.spark.SimpleSparkRouteCreator; +import uk.co.compendiumdev.thingifier.adapter.spark.conversion.InternalHttpResponseToSpark; +import uk.co.compendiumdev.thingifier.adapter.spark.conversion.SparkToInternalHttpRequest; import uk.co.compendiumdev.thingifier.api.docgen.RoutingDefinition; import uk.co.compendiumdev.thingifier.api.docgen.RoutingStatus; import uk.co.compendiumdev.thingifier.api.docgen.RoutingVerb; @@ -33,7 +34,6 @@ import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; import uk.co.compendiumdev.thingifier.core.domain.instances.validation.EntityInstanceStateValidator; import uk.co.compendiumdev.thingifier.core.reporting.ValidationReport; -import uk.co.compendiumdev.thingifier.spark.SimpleSparkRouteCreator; // TODO: This should be using a Thingifier to do the work of XML JSON etc... like the simulation public class AuthRoutes { diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerTrackingRoutes.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerTrackingRoutes.java index da7c73d3..8e5aaeb3 100644 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerTrackingRoutes.java +++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengerTrackingRoutes.java @@ -10,12 +10,12 @@ import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitions; import uk.co.compendiumdev.challenge.persistence.PersistenceLayer; import uk.co.compendiumdev.thingifier.Thingifier; +import uk.co.compendiumdev.thingifier.adapter.spark.SimpleSparkRouteCreator; import uk.co.compendiumdev.thingifier.api.docgen.*; import uk.co.compendiumdev.thingifier.api.response.ApiResponseAsJson; import uk.co.compendiumdev.thingifier.api.response.ApiResponseError; import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; -import uk.co.compendiumdev.thingifier.spark.SimpleSparkRouteCreator; public class ChallengerTrackingRoutes { diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengesRoutes.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengesRoutes.java index 2fd649cf..6a455576 100644 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengesRoutes.java +++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/ChallengesRoutes.java @@ -7,8 +7,9 @@ import uk.co.compendiumdev.challenge.apimodel.ChallengeThingifier; import uk.co.compendiumdev.challenge.challengers.Challengers; import uk.co.compendiumdev.challenge.challenges.ChallengeDefinitions; -import uk.co.compendiumdev.thingifier.adapter.http.routehandlers.SparkApiRequestResponseHandler; import uk.co.compendiumdev.thingifier.adapter.spark.AdhocDocumentedSparkRouteConfigurer; +import uk.co.compendiumdev.thingifier.adapter.spark.SimpleSparkRouteCreator; +import uk.co.compendiumdev.thingifier.adapter.spark.routehandlers.SparkApiRequestResponseHandler; import uk.co.compendiumdev.thingifier.api.docgen.RoutingDefinition; import uk.co.compendiumdev.thingifier.api.docgen.RoutingStatus; import uk.co.compendiumdev.thingifier.api.docgen.RoutingVerb; @@ -17,7 +18,6 @@ import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; import uk.co.compendiumdev.thingifier.core.query.FilterBy; import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; -import uk.co.compendiumdev.thingifier.spark.SimpleSparkRouteCreator; public class ChallengesRoutes { diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/HeartBeatRoutes.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/HeartBeatRoutes.java index 4f7c0f9f..42a31d16 100644 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/HeartBeatRoutes.java +++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/challengesrouting/HeartBeatRoutes.java @@ -2,9 +2,9 @@ import java.util.List; import uk.co.compendiumdev.thingifier.adapter.spark.AdhocDocumentedSparkRouteConfigurer; +import uk.co.compendiumdev.thingifier.adapter.spark.SimpleSparkRouteCreator; import uk.co.compendiumdev.thingifier.api.docgen.RoutingVerb; import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; -import uk.co.compendiumdev.thingifier.spark.SimpleSparkRouteCreator; public class HeartBeatRoutes { diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/MirrorRoutes.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/MirrorRoutes.java index f241fc45..da33d30f 100644 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/MirrorRoutes.java +++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/MirrorRoutes.java @@ -6,12 +6,12 @@ import java.util.List; import spark.Route; import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.http.ThingifierAutoDocGenRouting; import uk.co.compendiumdev.thingifier.adapter.spark.AdhocDocumentedSparkRouteConfigurer; +import uk.co.compendiumdev.thingifier.adapter.spark.SimpleSparkRouteCreator; +import uk.co.compendiumdev.thingifier.adapter.spark.ThingifierAutoDocGenRouting; import uk.co.compendiumdev.thingifier.api.docgen.RoutingVerb; import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; import uk.co.compendiumdev.thingifier.htmlgui.htmlgen.DefaultGUIHTML; -import uk.co.compendiumdev.thingifier.spark.SimpleSparkRouteCreator; /* Create a set of custom end points that do not use a thingifier, but diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/RequestMirror.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/RequestMirror.java index 4668fc5d..7703a1a7 100644 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/RequestMirror.java +++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/mirror/RequestMirror.java @@ -4,7 +4,7 @@ import spark.Response; import uk.co.compendiumdev.challenge.spark.SparkMessageLengthValidator; import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.http.routehandlers.SparkApiRequestResponseHandler; +import uk.co.compendiumdev.thingifier.adapter.spark.routehandlers.SparkApiRequestResponseHandler; import uk.co.compendiumdev.thingifier.api.http.headers.headerparser.AcceptHeaderParser; import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleApiRoutes.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleApiRoutes.java index c1d1f3e1..6d9906dc 100644 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleApiRoutes.java +++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleApiRoutes.java @@ -4,8 +4,9 @@ import java.util.List; import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.http.ThingifierAutoDocGenRouting; -import uk.co.compendiumdev.thingifier.adapter.http.ThingifierHttpApiRoutings; +import uk.co.compendiumdev.thingifier.adapter.spark.SimpleSparkRouteCreator; +import uk.co.compendiumdev.thingifier.adapter.spark.ThingifierAutoDocGenRouting; +import uk.co.compendiumdev.thingifier.adapter.spark.ThingifierHttpApiRoutings; import uk.co.compendiumdev.thingifier.api.docgen.RoutingDefinition; import uk.co.compendiumdev.thingifier.api.docgen.RoutingStatus; import uk.co.compendiumdev.thingifier.api.docgen.RoutingVerb; @@ -19,7 +20,6 @@ import uk.co.compendiumdev.thingifier.core.domain.definitions.validation.MaximumLengthValidationRule; import uk.co.compendiumdev.thingifier.htmlgui.htmlgen.DefaultGUIHTML; import uk.co.compendiumdev.thingifier.htmlgui.routing.DefaultGuiRoutings; -import uk.co.compendiumdev.thingifier.spark.SimpleSparkRouteCreator; /* The simple API is a no-auth API where anyone can amend, create, delete items. diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simulation/SimulationRoutes.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simulation/SimulationRoutes.java index 76c5195d..71c2522d 100644 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simulation/SimulationRoutes.java +++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simulation/SimulationRoutes.java @@ -5,9 +5,10 @@ import java.util.List; import uk.co.compendiumdev.challenge.ChallengerConfig; import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.http.ThingifierAutoDocGenRouting; import uk.co.compendiumdev.thingifier.adapter.http.routehandlers.HttpApiRequestHandler; -import uk.co.compendiumdev.thingifier.adapter.http.routehandlers.SparkApiRequestResponseHandler; +import uk.co.compendiumdev.thingifier.adapter.spark.SimpleSparkRouteCreator; +import uk.co.compendiumdev.thingifier.adapter.spark.ThingifierAutoDocGenRouting; +import uk.co.compendiumdev.thingifier.adapter.spark.routehandlers.SparkApiRequestResponseHandler; import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; import uk.co.compendiumdev.thingifier.api.response.ApiResponse; @@ -26,7 +27,6 @@ import uk.co.compendiumdev.thingifier.core.repository.ThingStore; import uk.co.compendiumdev.thingifier.core.repository.ThingStoreProviderConfig; import uk.co.compendiumdev.thingifier.htmlgui.htmlgen.DefaultGUIHTML; -import uk.co.compendiumdev.thingifier.spark.SimpleSparkRouteCreator; public class SimulationRoutes { diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/spark/SparkMessageLengthValidator.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/spark/SparkMessageLengthValidator.java index 23a403c1..8a4e8fdc 100644 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/spark/SparkMessageLengthValidator.java +++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/spark/SparkMessageLengthValidator.java @@ -5,8 +5,8 @@ import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpRequest; import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.HttpApiResponseToInternalHttpResponse; import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.InternalHttpRequestToHttpApiRequest; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.InternalHttpResponseToSpark; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.SparkToInternalHttpRequest; +import uk.co.compendiumdev.thingifier.adapter.spark.conversion.InternalHttpResponseToSpark; +import uk.co.compendiumdev.thingifier.adapter.spark.conversion.SparkToInternalHttpRequest; import uk.co.compendiumdev.thingifier.api.ermodelconversion.JsonThing; import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; import uk.co.compendiumdev.thingifier.api.http.HttpApiResponse; diff --git a/challengerAuto/src/test/java/uk/co/compendiumdev/challenger/restassured/_11_accept_challenges/C030GetTodosNotAcceptableHeader406Test.java b/challengerAuto/src/test/java/uk/co/compendiumdev/challenger/restassured/_11_accept_challenges/C030GetTodosNotAcceptableHeader406Test.java index 91549b06..6a13d103 100644 --- a/challengerAuto/src/test/java/uk/co/compendiumdev/challenger/restassured/_11_accept_challenges/C030GetTodosNotAcceptableHeader406Test.java +++ b/challengerAuto/src/test/java/uk/co/compendiumdev/challenger/restassured/_11_accept_challenges/C030GetTodosNotAcceptableHeader406Test.java @@ -2,7 +2,6 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; -import io.restassured.response.Response; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import uk.co.compendiumdev.challenger.restassured.api.ChallengesStatus; @@ -13,16 +12,13 @@ public class C030GetTodosNotAcceptableHeader406Test extends RestAssuredBaseTest @Test void canGet406WhenUnsupportedAcceptHeaderSent() { - final Response response = - RestAssured.given() - .header("X-CHALLENGER", xChallenger) - .accept("application/gzip") - .get(apiPath("/todos")) - .then() - .statusCode(406) - .contentType(ContentType.JSON) - .extract() - .response(); + RestAssured.given() + .header("X-CHALLENGER", xChallenger) + .accept("application/gzip") + .get(apiPath("/todos")) + .then() + .statusCode(406) + .contentType(ContentType.JSON); ChallengesStatus statuses = new ChallengesStatus(); statuses.get(); diff --git a/challengerAuto/src/test/java/uk/co/compendiumdev/challenger/restassured/_13_restore_session_challenges/C035PutRestoreChallengerSessionTest.java b/challengerAuto/src/test/java/uk/co/compendiumdev/challenger/restassured/_13_restore_session_challenges/C035PutRestoreChallengerSessionTest.java index b97dc30b..697f8c19 100644 --- a/challengerAuto/src/test/java/uk/co/compendiumdev/challenger/restassured/_13_restore_session_challenges/C035PutRestoreChallengerSessionTest.java +++ b/challengerAuto/src/test/java/uk/co/compendiumdev/challenger/restassured/_13_restore_session_challenges/C035PutRestoreChallengerSessionTest.java @@ -80,18 +80,14 @@ public void canPutChallengerSessionToReset() { challengerResponse.challengeStatus.CREATE_NEW_CHALLENGER = true; - nResponse = - RestAssured.given() - .header("X-CHALLENGER", xChallenger) - .accept("application/json") - .body(challengerResponse) - .put(apiPath("/challenger/" + xChallenger)) - .then() - .statusCode(200) - . // updated existing - contentType(ContentType.JSON) - .and() - .extract() - .response(); + RestAssured.given() + .header("X-CHALLENGER", xChallenger) + .accept("application/json") + .body(challengerResponse) + .put(apiPath("/challenger/" + xChallenger)) + .then() + .statusCode(200) + . // updated existing + contentType(ContentType.JSON); } } diff --git a/challengerAuto/src/test/java/uk/co/compendiumdev/challenger/restassured/_13_restore_session_challenges/C038PutRestoreChallengerTodosTest.java b/challengerAuto/src/test/java/uk/co/compendiumdev/challenger/restassured/_13_restore_session_challenges/C038PutRestoreChallengerTodosTest.java index 62e7ae04..e40754d3 100644 --- a/challengerAuto/src/test/java/uk/co/compendiumdev/challenger/restassured/_13_restore_session_challenges/C038PutRestoreChallengerTodosTest.java +++ b/challengerAuto/src/test/java/uk/co/compendiumdev/challenger/restassured/_13_restore_session_challenges/C038PutRestoreChallengerTodosTest.java @@ -18,18 +18,13 @@ public void canGetAllTodosAndPutTodosToRestore() { Assertions.assertNotNull(xChallenger); Assertions.assertTrue(xChallenger.length() > 5); - Todos todosResponse = - RestAssured.given() - .header("X-CHALLENGER", xChallenger) - .accept("application/json") - .get(apiPath("/challenger/database/" + xChallenger)) - .then() - .statusCode(200) - .contentType(ContentType.JSON) - .and() - .extract() - .response() - .as(Todos.class); + RestAssured.given() + .header("X-CHALLENGER", xChallenger) + .accept("application/json") + .get(apiPath("/challenger/database/" + xChallenger)) + .then() + .statusCode(200) + .contentType(ContentType.JSON); ChallengesStatus statuses = new ChallengesStatus(); statuses.get(); diff --git a/checkstyle-project-fqn.xml b/checkstyle-project-fqn.xml index 35b0fbd9..001e0e7a 100644 --- a/checkstyle-project-fqn.xml +++ b/checkstyle-project-fqn.xml @@ -68,7 +68,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingStore.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingStore.java index d4290ed8..da42cc04 100644 --- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingStore.java +++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingStore.java @@ -12,6 +12,8 @@ public interface ThingStore extends AutoCloseable { RepositoryAdministration administration(); + ThingStoreTransaction beginTransaction(); + @Override default void close() { // Most store implementations do not own closeable resources. diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingStoreTransaction.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingStoreTransaction.java new file mode 100644 index 00000000..2382fb55 --- /dev/null +++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingStoreTransaction.java @@ -0,0 +1,11 @@ +package uk.co.compendiumdev.thingifier.core.repository; + +public interface ThingStoreTransaction extends AutoCloseable { + + void commit(); + + void rollback(); + + @Override + void close(); +} diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryEntityInstanceCollection.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryEntityInstanceCollection.java index 587b34d0..34d1e564 100644 --- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryEntityInstanceCollection.java +++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryEntityInstanceCollection.java @@ -282,6 +282,27 @@ public Map getCounters() { return counters; } + Snapshot snapshot() { + Map counterValues = new HashMap<>(); + ensureCountersInitialized(); + for (Map.Entry counter : counters.entrySet()) { + counterValues.put(counter.getKey(), counter.getValue().peekNextValue()); + } + return new Snapshot(definition, new ArrayList<>(instances.values()), counterValues); + } + + void restore(final Snapshot snapshot) { + instances.clear(); + for (EntityInstance instance : snapshot.instances) { + instances.put(instance.getInternalId(), instance); + } + + counters.clear(); + for (Map.Entry counter : snapshot.counterValues.entrySet()) { + counters.put(counter.getKey(), new AutoIncrement(counter.getKey(), counter.getValue())); + } + } + public ValidationReport checkFieldsForUniqueNess(EntityInstance instance, boolean isAmendment) { ValidationReport report = new ValidationReport(); @@ -319,4 +340,20 @@ public ValidationReport checkFieldsForUniqueNess(EntityInstance instance, boolea return report; } + + static final class Snapshot { + + final EntityDefinition definition; + private final List instances; + private final Map counterValues; + + private Snapshot( + final EntityDefinition definition, + final List instances, + final Map counterValues) { + this.definition = definition; + this.instances = instances; + this.counterValues = counterValues; + } + } } diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryEntityInstanceStore.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryEntityInstanceStore.java index d24dad67..5f42726e 100644 --- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryEntityInstanceStore.java +++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryEntityInstanceStore.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema; import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; @@ -96,4 +97,37 @@ void clearInstanceDataFor(String instanceName) { deleteEntityInstance(instance); } } + + Snapshot snapshot() { + ConcurrentHashMap snapshots = + new ConcurrentHashMap<>(); + for (Map.Entry collection : + instanceCollections.entrySet()) { + snapshots.put(collection.getKey(), collection.getValue().snapshot()); + } + return new Snapshot(snapshots); + } + + void restore(final Snapshot snapshot) { + instanceCollections.clear(); + for (Map.Entry entry : + snapshot.collections.entrySet()) { + InMemoryEntityInstanceCollection collection = + new InMemoryEntityInstanceCollection(entry.getValue().definition); + collection.restore(entry.getValue()); + instanceCollections.put(entry.getKey(), collection); + } + } + + static final class Snapshot { + + private final ConcurrentHashMap + collections; + + private Snapshot( + final ConcurrentHashMap + collections) { + this.collections = collections; + } + } } diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryRelationshipStore.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryRelationshipStore.java index f1763ae4..a3094234 100644 --- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryRelationshipStore.java +++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryRelationshipStore.java @@ -126,6 +126,15 @@ void clear() { rows.clear(); } + Snapshot snapshot() { + return new Snapshot(new LinkedHashMap<>(rows)); + } + + void restore(final Snapshot snapshot) { + rows.clear(); + rows.putAll(snapshot.rows); + } + private List rowsInvolving(final EntityInstance instance) { List involving = new ArrayList<>(); for (RelationshipRow row : rows.values()) { @@ -170,4 +179,13 @@ private String rowKey( + "|" + toInternalId; } + + static final class Snapshot { + + private final Map rows; + + private Snapshot(final Map rows) { + this.rows = rows; + } + } } diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryThingStore.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryThingStore.java index 48447e7f..ae4a70a6 100644 --- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryThingStore.java +++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/inmemory/InMemoryThingStore.java @@ -21,6 +21,7 @@ import uk.co.compendiumdev.thingifier.core.repository.RelationshipRepository; import uk.co.compendiumdev.thingifier.core.repository.RepositoryAdministration; import uk.co.compendiumdev.thingifier.core.repository.ThingStore; +import uk.co.compendiumdev.thingifier.core.repository.ThingStoreTransaction; import uk.co.compendiumdev.thingifier.core.repository.validation.EntityInstanceWriteValidator; public class InMemoryThingStore implements ThingStore { @@ -74,6 +75,11 @@ public RepositoryAdministration administration() { return administration; } + @Override + public ThingStoreTransaction beginTransaction() { + return new InMemoryThingStoreTransaction(instanceData.snapshot(), relationships.snapshot()); + } + void initializeFrom(final ERSchema schema) { refreshSchema(schema); } @@ -362,4 +368,40 @@ private EntityInstance findByInternalId( } return collection.findInstanceByInternalID(internalId); } + + private final class InMemoryThingStoreTransaction implements ThingStoreTransaction { + + private final InMemoryEntityInstanceStore.Snapshot instanceSnapshot; + private final InMemoryRelationshipStore.Snapshot relationshipSnapshot; + private boolean completed; + + private InMemoryThingStoreTransaction( + final InMemoryEntityInstanceStore.Snapshot instanceSnapshot, + final InMemoryRelationshipStore.Snapshot relationshipSnapshot) { + this.instanceSnapshot = instanceSnapshot; + this.relationshipSnapshot = relationshipSnapshot; + } + + @Override + public void commit() { + completed = true; + } + + @Override + public void rollback() { + if (completed) { + return; + } + relationships.restore(relationshipSnapshot); + instanceData.restore(instanceSnapshot); + completed = true; + } + + @Override + public void close() { + if (!completed) { + rollback(); + } + } + } } diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/sqlite/SqliteThingStore.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/sqlite/SqliteThingStore.java index a104106d..691d308b 100644 --- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/sqlite/SqliteThingStore.java +++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/sqlite/SqliteThingStore.java @@ -42,6 +42,7 @@ import uk.co.compendiumdev.thingifier.core.repository.RelationshipRepository; import uk.co.compendiumdev.thingifier.core.repository.RepositoryAdministration; import uk.co.compendiumdev.thingifier.core.repository.ThingStore; +import uk.co.compendiumdev.thingifier.core.repository.ThingStoreTransaction; import uk.co.compendiumdev.thingifier.core.repository.relationship.RelationshipEndpoint; import uk.co.compendiumdev.thingifier.core.repository.relationship.RelationshipRow; import uk.co.compendiumdev.thingifier.core.repository.relationship.RelationshipRules; @@ -114,6 +115,23 @@ public RepositoryAdministration administration() { return administration; } + @Override + public ThingStoreTransaction beginTransaction() { + openConnection(); + try { + boolean originalAutoCommit = connection.getAutoCommit(); + Map materializedSnapshot = new HashMap<>(materializedInstances); + Map> counterSnapshot = snapshotCounters(); + if (originalAutoCommit) { + connection.setAutoCommit(false); + } + return new SqliteThingStoreTransaction( + originalAutoCommit, materializedSnapshot, counterSnapshot); + } catch (SQLException e) { + throw new IllegalStateException("Could not begin SQLite transaction", e); + } + } + void initializeFrom(final ERSchema schema) { this.schema = schema; openConnection(); @@ -1488,6 +1506,10 @@ private void executePreparedSql(final String sql, final List parameters) private void runInTransaction(final Runnable operation) { try { boolean originalAutoCommit = connection.getAutoCommit(); + if (!originalAutoCommit) { + operation.run(); + return; + } connection.setAutoCommit(false); try { operation.run(); @@ -1506,6 +1528,101 @@ private void runInTransaction(final Runnable operation) { } } + private Map> snapshotCounters() { + Map> snapshot = new HashMap<>(); + for (Map.Entry> entityCounters : + countersByEntity.entrySet()) { + Map fieldCounters = new HashMap<>(); + for (Map.Entry counter : entityCounters.getValue().entrySet()) { + fieldCounters.put(counter.getKey(), counter.getValue().peekNextValue()); + } + snapshot.put(entityCounters.getKey(), fieldCounters); + } + return snapshot; + } + + private void restoreCounters(final Map> snapshot) { + countersByEntity.clear(); + for (Map.Entry> entityCounters : snapshot.entrySet()) { + Map fieldCounters = new HashMap<>(); + for (Map.Entry counter : entityCounters.getValue().entrySet()) { + fieldCounters.put( + counter.getKey(), new AutoIncrement(counter.getKey(), counter.getValue())); + } + countersByEntity.put(entityCounters.getKey(), fieldCounters); + } + } + + private final class SqliteThingStoreTransaction implements ThingStoreTransaction { + + private final boolean restoreAutoCommitOnClose; + private final Map materializedSnapshot; + private final Map> counterSnapshot; + private boolean completed; + + private SqliteThingStoreTransaction( + final boolean restoreAutoCommitOnClose, + final Map materializedSnapshot, + final Map> counterSnapshot) { + this.restoreAutoCommitOnClose = restoreAutoCommitOnClose; + this.materializedSnapshot = materializedSnapshot; + this.counterSnapshot = counterSnapshot; + } + + @Override + public void commit() { + if (completed) { + return; + } + try { + if (restoreAutoCommitOnClose) { + connection.commit(); + } + completed = true; + } catch (SQLException e) { + throw new IllegalStateException("Could not commit SQLite transaction", e); + } finally { + restoreAutoCommit(); + } + } + + @Override + public void rollback() { + if (completed) { + return; + } + try { + connection.rollback(); + materializedInstances.clear(); + materializedInstances.putAll(materializedSnapshot); + restoreCounters(counterSnapshot); + completed = true; + } catch (SQLException e) { + throw new IllegalStateException("Could not rollback SQLite transaction", e); + } finally { + restoreAutoCommit(); + } + } + + @Override + public void close() { + if (!completed) { + rollback(); + } + } + + private void restoreAutoCommit() { + if (!restoreAutoCommitOnClose) { + return; + } + try { + connection.setAutoCommit(true); + } catch (SQLException e) { + throw new IllegalStateException("Could not restore SQLite auto commit", e); + } + } + } + private String sqlType(final FieldType type) { switch (type) { case AUTO_INCREMENT: diff --git a/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/repository/ThingStoreContractTest.java b/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/repository/ThingStoreContractTest.java index d9b69a64..bc8b5f3a 100644 --- a/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/repository/ThingStoreContractTest.java +++ b/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/repository/ThingStoreContractTest.java @@ -58,6 +58,36 @@ public void sqliteProviderKeepsLogicalDatabasesSeparate() { } } + @Test + public void inMemoryTransactionCommitPersistsChanges() { + ThingStore repository = new InMemoryThingStore(EntityRelModel.DEFAULT_DATABASE_NAME); + + exerciseTransactionCommit(repository); + } + + @Test + public void sqliteTransactionCommitPersistsChanges() { + try (ThingStore repository = + SqliteThingStore.inMemory(EntityRelModel.DEFAULT_DATABASE_NAME)) { + exerciseTransactionCommit(repository); + } + } + + @Test + public void inMemoryTransactionRollbackRestoresEntitiesRelationshipsAndCounters() { + ThingStore repository = new InMemoryThingStore(EntityRelModel.DEFAULT_DATABASE_NAME); + + exerciseTransactionRollback(repository); + } + + @Test + public void sqliteTransactionRollbackRestoresEntitiesRelationshipsAndCounters() { + try (ThingStore repository = + SqliteThingStore.inMemory(EntityRelModel.DEFAULT_DATABASE_NAME)) { + exerciseTransactionRollback(repository); + } + } + @Test public void sqliteRepositoryCannotBeUsedAfterClose() { ERSchema schema = todoSchema(); @@ -755,6 +785,39 @@ private void addProjectAndTask(final ThingStore repository, final ERSchema schem repository.relationships().connect(project, "tasks", task); } + private void exerciseTransactionCommit(final ThingStore repository) { + ERSchema schema = todoSchema(); + repository.administration().initializeFrom(schema); + EntityDefinition project = schema.getEntityDefinitionNamed("project"); + + try (ThingStoreTransaction transaction = repository.beginTransaction()) { + create(repository, project, "Committed project"); + transaction.commit(); + } + + Assertions.assertEquals(1, repository.entityQueries().count(project)); + } + + private void exerciseTransactionRollback(final ThingStore repository) { + ERSchema schema = todoSchema(); + repository.administration().initializeFrom(schema); + EntityDefinition project = schema.getEntityDefinitionNamed("project"); + EntityDefinition task = schema.getEntityDefinitionNamed("task"); + + try (ThingStoreTransaction transaction = repository.beginTransaction()) { + EntityInstance projectInstance = create(repository, project, "Rolled back project"); + EntityInstance taskInstance = create(repository, task, "Rolled back task"); + repository.relationships().connect(projectInstance, "tasks", taskInstance); + transaction.rollback(); + } + + Assertions.assertEquals(0, repository.entityQueries().count(project)); + Assertions.assertEquals(0, repository.entityQueries().count(task)); + + EntityInstance taskAfterRollback = create(repository, task, "Counter starts again"); + Assertions.assertEquals("1", taskAfterRollback.getPrimaryKeyValue()); + } + private EntityInstance create( final ThingStore repository, final EntityDefinition entity, final String title) { return repository diff --git a/standAloneTodoListManagerRestApi/src/main/java/uk/co/compendiumdev/todolist/application/Main.java b/standAloneTodoListManagerRestApi/src/main/java/uk/co/compendiumdev/todolist/application/Main.java index 22fa5d0a..f67fd96b 100644 --- a/standAloneTodoListManagerRestApi/src/main/java/uk/co/compendiumdev/todolist/application/Main.java +++ b/standAloneTodoListManagerRestApi/src/main/java/uk/co/compendiumdev/todolist/application/Main.java @@ -1,9 +1,7 @@ package uk.co.compendiumdev.todolist.application; import static spark.Spark.get; -import static spark.route.HttpMethod.get; -import uk.co.compendiumdev.thingifier.Thingifier; import uk.co.compendiumdev.thingifier.adapter.spark.MainImplementation; import uk.co.compendiumdev.thingifier.application.examples.TodoManagerThingifier; @@ -19,7 +17,7 @@ public static void main(String[] args) { app.configurePortAndDefaultRoutes(); app.setupBuiltInConfigurableRoutes(); - Thingifier thingifier = app.chooseThingifier(); + app.chooseThingifier(); app.configureThingifierWithProfile(); app.setupDefaultGui(); diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/bootstrap/ThingifierServerBootstrap.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/bootstrap/ThingifierServerBootstrap.java new file mode 100644 index 00000000..f8bd5f53 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/bootstrap/ThingifierServerBootstrap.java @@ -0,0 +1,25 @@ +package uk.co.compendiumdev.thingifier.adapter.bootstrap; + +import uk.co.compendiumdev.thingifier.Thingifier; +import uk.co.compendiumdev.thingifier.adapter.spark.SparkHttpGenericExceptionRoutings; +import uk.co.compendiumdev.thingifier.adapter.spark.ThingifierAutoDocGenRouting; +import uk.co.compendiumdev.thingifier.adapter.spark.ThingifierHttpApiRoutings; +import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; +import uk.co.compendiumdev.thingifier.htmlgui.htmlgen.DefaultGUIHTML; + +public final class ThingifierServerBootstrap { + + public ThingifierHttpApiRoutings startRestServer( + final Thingifier thingifier, + final ThingifierApiDocumentationDefn apiDefn, + final DefaultGUIHTML guiManagement) { + + apiDefn.setThingifier(thingifier); + + new ThingifierAutoDocGenRouting(thingifier, apiDefn, guiManagement); + ThingifierHttpApiRoutings restServer = new ThingifierHttpApiRoutings(thingifier, apiDefn); + new SparkHttpGenericExceptionRoutings(); + + return restServer; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ApiMappingError.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ApiMappingError.java similarity index 94% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ApiMappingError.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ApiMappingError.java index 7fd9afd7..e5207e14 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ApiMappingError.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ApiMappingError.java @@ -1,4 +1,4 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; import java.util.ArrayList; import java.util.Collection; diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ApiRouteDisplay.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ApiRouteDisplay.java new file mode 100644 index 00000000..30bbf434 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ApiRouteDisplay.java @@ -0,0 +1,52 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; + +public final class ApiRouteDisplay { + + private static final ApiRouteDisplay EMPTY = new ApiRouteDisplay(""); + + private final String originalPath; + private final String missingInstanceMessage; + + private ApiRouteDisplay(final String originalPath) { + this(originalPath, ""); + } + + private ApiRouteDisplay(final String originalPath, final String missingInstanceMessage) { + this.originalPath = originalPath == null ? "" : originalPath; + this.missingInstanceMessage = missingInstanceMessage == null ? "" : missingInstanceMessage; + } + + public static ApiRouteDisplay empty() { + return EMPTY; + } + + public static ApiRouteDisplay originalPath(final String originalPath) { + if (originalPath == null || originalPath.isEmpty()) { + return EMPTY; + } + return new ApiRouteDisplay(originalPath); + } + + public static ApiRouteDisplay missingInstanceMessage(final String message) { + if (message == null || message.isEmpty()) { + return EMPTY; + } + return new ApiRouteDisplay("", message); + } + + public String originalPath() { + return originalPath; + } + + public boolean hasOriginalPath() { + return !originalPath.isEmpty(); + } + + public String missingInstanceMessage() { + return missingInstanceMessage; + } + + public boolean hasMissingInstanceMessage() { + return !missingInstanceMessage.isEmpty(); + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/DefaultThingifierApiRuntime.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/DefaultThingifierApiRuntime.java new file mode 100644 index 00000000..75106d4d --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/DefaultThingifierApiRuntime.java @@ -0,0 +1,55 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; + +import java.util.List; +import uk.co.compendiumdev.thingifier.Thingifier; +import uk.co.compendiumdev.thingifier.api.http.ThingifierRequestContext; +import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock; +import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; +import uk.co.compendiumdev.thingifier.application.ThingCommandService; +import uk.co.compendiumdev.thingifier.application.ThingQueryService; + +public final class DefaultThingifierApiRuntime implements ThingifierApiRuntime { + + private final Thingifier thingifier; + private final SchemaCatalog schema; + private final ThingQueryService queryService; + + public DefaultThingifierApiRuntime(final Thingifier thingifier) { + this.thingifier = thingifier; + this.schema = new ThingifierSchemaCatalog(thingifier); + this.queryService = new ThingQueryService(schema); + } + + @Override + public SchemaCatalog schema() { + return schema; + } + + @Override + public ThingifierApiConfig apiConfig() { + return thingifier.apiConfig(); + } + + @Override + public List thingNames() { + return thingifier.getThingNames(); + } + + @Override + public ThingifierRequestContext contextFrom(final HttpHeadersBlock requestHeaders) { + return ThingifierRequestContext.from(thingifier, requestHeaders); + } + + @Override + public ThingCommandService commandService(final ThingifierRequestContext context) { + return new ThingCommandService( + context.store(), + schema, + thingifier.apiConfig().willApiEnforceDeclaredTypesInInput()); + } + + @Override + public ThingQueryService queryService() { + return queryService; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/FieldValues.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/FieldValues.java similarity index 89% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/FieldValues.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/FieldValues.java index ba17ee1f..d50d7bcd 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/FieldValues.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/FieldValues.java @@ -1,4 +1,4 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; import java.util.ArrayList; import java.util.List; diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipBodyCommandParser.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/RelationshipBodyCommandParser.java similarity index 66% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipBodyCommandParser.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/RelationshipBodyCommandParser.java index 19dfbbd3..94a87921 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipBodyCommandParser.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/RelationshipBodyCommandParser.java @@ -1,35 +1,25 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; import java.util.ArrayList; import java.util.List; import java.util.Map; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser; import uk.co.compendiumdev.thingifier.application.command.RelationshipReference; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; -import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipVectorDefinition; +import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef; +import uk.co.compendiumdev.thingifier.application.schema.FieldSpec; +import uk.co.compendiumdev.thingifier.application.schema.RelationshipSpec; +import uk.co.compendiumdev.thingifier.application.schema.SchemaViewCatalog; import uk.co.compendiumdev.thingifier.core.reporting.ValidationReport; public final class RelationshipBodyCommandParser { - private final SchemaCatalog schema; + private final SchemaViewCatalog schema; - public RelationshipBodyCommandParser(final Thingifier thingifier) { - this(new ThingifierSchemaCatalog(thingifier)); - } - - RelationshipBodyCommandParser(final SchemaCatalog schema) { + public RelationshipBodyCommandParser(final SchemaViewCatalog schema) { this.schema = schema; } public RelationshipBodyCommands parse( - final BodyParser bodyargs, final EntityDefinition entity) { - return parse(bodyargs.getFlattenedStringMap(), entity); - } - - public RelationshipBodyCommands parse( - final List> flattenedArgs, final EntityDefinition entity) { + final List> flattenedArgs, final EntityTypeRef entity) { List> relationshipEntries = new ArrayList<>(); List references = new ArrayList<>(); ValidationReport report = new ValidationReport(); @@ -41,7 +31,7 @@ public RelationshipBodyCommands parse( parseComplexRelationship(entity, keyValue, report, references); } else if (key.contains(".")) { String[] parts = key.split("\\."); - if (parts.length > 0 && entity.related().hasRelationship(parts[0])) { + if (parts.length > 0 && entity.hasRelationship(parts[0])) { relationshipEntries.add(keyValue); parseCompressedRelationship(entity, keyValue, report, references); } @@ -53,7 +43,7 @@ public RelationshipBodyCommands parse( } private void parseCompressedRelationship( - final EntityDefinition entity, + final EntityTypeRef entity, final Map.Entry keyValue, final ValidationReport report, final List references) { @@ -80,7 +70,7 @@ private void parseCompressedRelationship( } private void parseComplexRelationship( - final EntityDefinition entity, + final EntityTypeRef entity, final Map.Entry keyValue, final ValidationReport report, final List references) { @@ -98,8 +88,7 @@ private void parseComplexRelationship( return; } - EntityDefinition relationshipTo = - schema.definitionWithSingularOrPluralNamed(relationshipToTerm); + EntityTypeRef relationshipTo = schema.entityWithSingularOrPluralName(relationshipToTerm); if (relationshipTo == null || !validRelationshipBetweenThings( entity, relationshipName, relationshipTo, report)) { @@ -109,20 +98,22 @@ private void parseComplexRelationship( references.add( RelationshipReference.explicit( relationshipName, - relationshipTo, + relationshipTo.name(), relationshipToTerm, relationshipField, keyValue.getValue())); } private boolean supportsReferenceField( - final EntityDefinition entity, final String relationshipName, final String fieldName) { - for (RelationshipVectorDefinition vector : - entity.related().getRelationships(relationshipName)) { - List linkingFields = - vector.getTo() - .getFieldNamesOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID); - if (linkingFields.contains(fieldName)) { + final EntityTypeRef entity, final String relationshipName, final String fieldName) { + for (RelationshipSpec relationship : entity.relationships()) { + if (!relationship.name().equals(relationshipName)) { + continue; + } + EntityTypeRef target = + schema.entityWithSingularOrPluralName(relationship.toEntityName()); + FieldSpec field = target == null ? null : target.fieldNamed(fieldName); + if (field != null && field.isProtectedField()) { return true; } } @@ -130,14 +121,14 @@ private boolean supportsReferenceField( } private boolean isValidRelationship( - final EntityDefinition entity, + final EntityTypeRef entity, final String relationshipName, final ValidationReport report) { - if (!entity.related().hasRelationship(relationshipName)) { + if (!entity.hasRelationship(relationshipName)) { report.addErrorMessage( String.format( "%s is not a valid relationship for %s", - relationshipName, entity.getName())); + relationshipName, entity.name())); return false; } return true; @@ -150,13 +141,13 @@ private void reportIsNotValidRelationship( } private boolean validRelationshipBetweenThings( - final EntityDefinition entity, + final EntityTypeRef entity, final String relationshipName, - final EntityDefinition relatedEntity, + final EntityTypeRef relatedEntity, final ValidationReport report) { - for (RelationshipVectorDefinition relationship : - entity.related().getRelationships(relationshipName)) { - if (relationship.getTo() == relatedEntity) { + for (RelationshipSpec relationship : entity.relationships()) { + if (relationship.name().equals(relationshipName) + && relationship.toEntityName().equals(relatedEntity.name())) { return true; } } @@ -164,7 +155,7 @@ private boolean validRelationshipBetweenThings( report.addErrorMessage( String.format( "%s to %s is not a valid relationship for %s", - relationshipName, relatedEntity.getPlural(), entity.getName())); + relationshipName, relatedEntity.pluralName(), entity.name())); return false; } } diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipBodyCommands.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/RelationshipBodyCommands.java similarity index 94% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipBodyCommands.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/RelationshipBodyCommands.java index 1ee5667a..c7a60016 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipBodyCommands.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/RelationshipBodyCommands.java @@ -1,4 +1,4 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; import java.util.ArrayList; import java.util.List; diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/SchemaCatalog.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/SchemaCatalog.java new file mode 100644 index 00000000..7ce11d4b --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/SchemaCatalog.java @@ -0,0 +1,18 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; + +import uk.co.compendiumdev.thingifier.application.schema.SchemaDefinitionResolver; +import uk.co.compendiumdev.thingifier.application.schema.SchemaViewCatalog; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; + +public interface SchemaCatalog extends SchemaViewCatalog, SchemaDefinitionResolver { + + EntityDefinition definitionWithSingularOrPluralNamed(String term); + + @Override + default EntityDefinition definitionWithSingularOrPluralName(final String term) { + return definitionWithSingularOrPluralNamed(term); + } + + @Override + boolean hasRelationshipNamed(String name); +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingBodyCommandMapper.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingBodyCommandMapper.java new file mode 100644 index 00000000..42c28d14 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingBodyCommandMapper.java @@ -0,0 +1,172 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import uk.co.compendiumdev.thingifier.api.http.bodyparser.ApiBodyField; +import uk.co.compendiumdev.thingifier.api.http.bodyparser.ApiBodyFields; +import uk.co.compendiumdev.thingifier.application.command.AmendThingCommand; +import uk.co.compendiumdev.thingifier.application.command.BodyFieldValue; +import uk.co.compendiumdev.thingifier.application.command.CreateAndConnectRelationshipCommand; +import uk.co.compendiumdev.thingifier.application.command.CreateThingCommand; +import uk.co.compendiumdev.thingifier.application.command.ReplaceThingCommand; +import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef; +import uk.co.compendiumdev.thingifier.application.schema.SchemaViewCatalog; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; +import uk.co.compendiumdev.thingifier.core.reporting.ValidationReport; + +public final class ThingBodyCommandMapper { + + private final SchemaViewCatalog schema; + + public ThingBodyCommandMapper(final SchemaViewCatalog schema) { + this.schema = schema; + } + + public ThingWriteRequestMapping mapCreate( + final ApiBodyFields bodyFields, + final EntityTypeRef entity, + final boolean validateFinalRelationships) { + RelationshipBodyCommands relationships = parseRelationships(bodyFields, entity); + if (!relationships.validationReport().isValid()) { + return invalidRelationships(relationships.validationReport()); + } + + return ThingWriteRequestMapping.command( + new CreateThingCommand( + entity.name(), + "", + fieldValuesExcludingRelationships(bodyFields, relationships), + bodyFieldValues(bodyFields), + relationships.references(), + validateFinalRelationships)); + } + + public ThingWriteRequestMapping mapCreateWithPrimaryKey( + final String primaryKey, final ApiBodyFields bodyFields, final EntityTypeRef entity) { + RelationshipBodyCommands relationships = parseRelationships(bodyFields, entity); + if (!relationships.validationReport().isValid()) { + return invalidRelationships(relationships.validationReport()); + } + + return ThingWriteRequestMapping.command( + new CreateThingCommand( + entity.name(), + primaryKey, + fieldValuesExcludingRelationships(bodyFields, relationships), + bodyFieldValues(bodyFields), + relationships.references(), + true)); + } + + public ThingWriteRequestMapping mapAmend( + final ApiBodyFields bodyFields, + final EntityTypeRef entity, + final String identifier, + final boolean replaceExistingFieldsAndRelationships) { + RelationshipBodyCommands relationships = parseRelationships(bodyFields, entity); + if (!relationships.validationReport().isValid()) { + return ThingWriteRequestMapping.error( + ApiMappingError.withMessages( + 400, relationships.validationReport().getErrorMessages())); + } + + return ThingWriteRequestMapping.command( + new AmendThingCommand( + entity.name(), + identifier, + fieldValuesExcludingRelationships(bodyFields, relationships), + bodyFieldValues(bodyFields), + replaceExistingFieldsAndRelationships, + relationships.references())); + } + + public ThingWriteRequestMapping mapPut( + final ApiBodyFields bodyFields, final EntityTypeRef entity, final String identifier) { + RelationshipBodyCommands relationships = parseRelationships(bodyFields, entity); + if (!relationships.validationReport().isValid()) { + return ThingWriteRequestMapping.error( + ApiMappingError.withMessages( + 400, relationships.validationReport().getErrorMessages())); + } + + return ThingWriteRequestMapping.command( + new ReplaceThingCommand( + entity.name(), + identifier, + fieldValuesExcludingRelationships(bodyFields, relationships), + bodyFieldValues(bodyFields), + relationships.references())); + } + + public ThingWriteRequestMapping mapCreateAndConnect( + final ApiBodyFields bodyFields, + final EntityTypeRef parentEntity, + final String parentIdentifier, + final String relationshipName, + final EntityTypeRef childEntity) { + RelationshipBodyCommands relationships = parseRelationships(bodyFields, childEntity); + if (!relationships.validationReport().isValid()) { + return invalidRelationships(relationships.validationReport()); + } + + return ThingWriteRequestMapping.command( + new CreateAndConnectRelationshipCommand( + parentEntity.name(), + parentIdentifier, + relationshipName, + childEntity.name(), + fieldValuesExcludingRelationships(bodyFields, relationships), + bodyFieldValues(bodyFields), + relationships.references())); + } + + public RelationshipBodyCommands parseRelationships( + final ApiBodyFields bodyFields, final EntityTypeRef entity) { + return new RelationshipBodyCommandParser(schema) + .parse(bodyFields.asFlattenedStringMap(), entity); + } + + private ThingWriteRequestMapping invalidRelationships(final ValidationReport validation) { + return ThingWriteRequestMapping.error( + ApiMappingError.withMessage( + 400, + String.format( + "Invalid relationships: %s", + validation.getCombinedErrorMessages()))); + } + + public List fieldValuesExcludingRelationships( + final ApiBodyFields bodyFields, final RelationshipBodyCommands relationships) { + List> flattenedArgs = + new ArrayList<>(bodyFields.asFlattenedStringMap()); + flattenedArgs.removeAll(relationships.relationshipEntries()); + return FieldValues.fromListMapEntryStringString(flattenedArgs); + } + + public List bodyFieldValues(final ApiBodyFields bodyFields) { + List values = new ArrayList<>(); + for (ApiBodyField field : bodyFields.topLevelFields()) { + values.add( + new BodyFieldValue( + field.name(), field.value(), sourceTypeFor(field.sourceType()))); + } + return values; + } + + private BodyFieldValue.SourceType sourceTypeFor(final String sourceType) { + if ("STRING".equals(sourceType)) { + return BodyFieldValue.SourceType.STRING; + } + if ("BOOLEAN".equals(sourceType)) { + return BodyFieldValue.SourceType.BOOLEAN; + } + if ("INTEGER".equals(sourceType)) { + return BodyFieldValue.SourceType.INTEGER; + } + if ("NUMERIC".equals(sourceType)) { + return BodyFieldValue.SourceType.NUMERIC; + } + return BodyFieldValue.SourceType.SOMETHING_ELSE; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingCommandResultApiMapper.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingCommandResultApiMapper.java new file mode 100644 index 00000000..3f7d4e42 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingCommandResultApiMapper.java @@ -0,0 +1,203 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; + +import java.util.List; +import uk.co.compendiumdev.thingifier.api.response.ApiResponse; +import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; +import uk.co.compendiumdev.thingifier.application.ApplicationError; +import uk.co.compendiumdev.thingifier.application.ThingCommandResult; +import uk.co.compendiumdev.thingifier.application.command.AmendThingCommand; +import uk.co.compendiumdev.thingifier.application.command.ConnectExistingRelationshipCommand; +import uk.co.compendiumdev.thingifier.application.command.CreateAndConnectRelationshipCommand; +import uk.co.compendiumdev.thingifier.application.command.CreateThingCommand; +import uk.co.compendiumdev.thingifier.application.command.DeleteThingCommand; +import uk.co.compendiumdev.thingifier.application.command.DisconnectRelationshipCommand; +import uk.co.compendiumdev.thingifier.application.command.RelateThingCommand; +import uk.co.compendiumdev.thingifier.application.command.ReplaceThingCommand; +import uk.co.compendiumdev.thingifier.application.command.ThingWriteCommand; + +public final class ThingCommandResultApiMapper { + + private static final String CREATED_ITEM_ROLLED_BACK_MESSAGE = + " the newly created item was deleted. No new items have been created."; + private static final String VALIDATION_PREFIX = "Failed Validation: "; + + private final ThingifierApiConfig apiConfig; + + public ThingCommandResultApiMapper(final ThingifierApiConfig apiConfig) { + this.apiConfig = apiConfig; + } + + public ApiResponse map(final ApiMappingError error) { + return ApiResponse.error(error.statusCode(), error.messages()); + } + + public ApiResponse map( + final ThingWriteRequestMapping mapping, final ThingCommandResult result) { + return map(mapping.getCommand(), result, mapping.getRouteDisplay()); + } + + public ApiResponse map(final ThingWriteCommand command, final ThingCommandResult result) { + return map(command, result, ApiRouteDisplay.empty()); + } + + private ApiResponse map( + final ThingWriteCommand command, + final ThingCommandResult result, + final ApiRouteDisplay routeDisplay) { + if (result.isError()) { + return errorResponseFor(command, result, routeDisplay); + } + + if (command instanceof RelateThingCommand) { + return ApiResponse.created( + result.createdInstance() ? result.getInstance() : null, apiConfig); + } + + if (command instanceof CreateThingCommand + || command instanceof CreateAndConnectRelationshipCommand) { + return ApiResponse.created(result.getInstance(), apiConfig); + } + + if (command instanceof ConnectExistingRelationshipCommand) { + return ApiResponse.created(null, apiConfig); + } + + if (command instanceof ReplaceThingCommand) { + if (result.createdInstance()) { + return ApiResponse.created(result.getInstance(), apiConfig); + } + return ApiResponse.success().returnSingleInstance(result.getInstance()); + } + + if (command instanceof AmendThingCommand) { + return ApiResponse.success().returnSingleInstance(result.getInstance()); + } + + if (command instanceof DeleteThingCommand + || command instanceof DisconnectRelationshipCommand) { + return ApiResponse.success(); + } + + return ApiResponse.error(400, result.getErrorMessages()); + } + + private ApiResponse errorResponseFor( + final ThingWriteCommand command, + final ThingCommandResult result, + final ApiRouteDisplay routeDisplay) { + List messages = errorMessagesFor(result, routeDisplay); + int statusCode = statusFor(result.getError()); + ApiResponse response; + if (command instanceof CreateThingCommand && messages.size() == 1) { + response = + ApiResponse.error( + statusCode, creationErrorMessage(String.join(", ", messages))); + } else { + response = ApiResponse.error(statusCode, messages); + } + + if (result.rolledBackCreatedInstance()) { + response.addToErrorMessages(CREATED_ITEM_ROLLED_BACK_MESSAGE); + } + return response; + } + + public static int statusFor(final ApplicationError error) { + if (error == null) { + return 400; + } + if (error.category() == ApplicationError.Category.NOT_FOUND) { + return 404; + } + if (error.category() == ApplicationError.Category.CONFLICT) { + return 409; + } + return 400; + } + + private List errorMessagesFor( + final ThingCommandResult result, final ApiRouteDisplay routeDisplay) { + ApplicationError error = result.getError(); + if (error == null) { + return result.getErrorMessages(); + } + + if (error.code() == ApplicationError.Code.REPLACE_CREATE_AUTO_FIELDS_NOT_ALLOWED) { + return List.of( + String.format( + "Cannot create %s with PUT due to Auto fields %s", + error.detail("entityName"), error.detail("fieldNames"))); + } + + if (error.code() == ApplicationError.Code.REPLACE_CREATE_KEY_MISMATCH) { + return List.of( + String.format( + "Cannot create %s with PUT as key does not match body value %s != %s", + error.detail("entityName"), + error.detail("routeIdentifier"), + error.detail("bodyIdentifier"))); + } + + if (error.code() == ApplicationError.Code.INSTANCE_NOT_FOUND) { + if (routeDisplay.hasMissingInstanceMessage()) { + return List.of(routeDisplay.missingInstanceMessage()); + } + return List.of( + String.format( + "Could not find any instances with %s", + displayPathOrIdentifier(routeDisplay, error.detail("identifier")))); + } + + if (error.code() == ApplicationError.Code.PARENT_INSTANCE_NOT_FOUND) { + if (routeDisplay.hasOriginalPath()) { + return List.of( + String.format( + "Could not find parent thing for relationship %s", + routeDisplay.originalPath())); + } + return result.getErrorMessages(); + } + + if (error.code() == ApplicationError.Code.RELATIONSHIP_SOURCE_NOT_FOUND + || error.code() == ApplicationError.Code.RELATIONSHIP_TARGET_NOT_FOUND) { + return List.of( + String.format( + "Could not find any instances with %s", + displayPathOrIdentifier(routeDisplay, error.detail("identifier")))); + } + + return result.getErrorMessages(); + } + + private String displayPathOrIdentifier( + final ApiRouteDisplay routeDisplay, final String identifier) { + if (routeDisplay.hasOriginalPath()) { + return routeDisplay.originalPath(); + } + return identifier == null ? "" : identifier; + } + + private String creationErrorMessage(final String rawMessage) { + String message = rawMessage; + if (message == null) { + return ""; + } + + if (message.startsWith(VALIDATION_PREFIX) + && message.substring(VALIDATION_PREFIX.length()).endsWith(" : field is mandatory") + && !message.substring(VALIDATION_PREFIX.length()).contains(", ")) { + return message.substring(VALIDATION_PREFIX.length()); + } + if (isPlainValidationMessage(message)) { + return VALIDATION_PREFIX + message; + } + return message; + } + + private boolean isPlainValidationMessage(final String message) { + return message != null + && !message.startsWith(VALIDATION_PREFIX) + && !message.startsWith("ERROR:") + && !message.startsWith("Cannot Create"); + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingReadRequestMapper.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingReadRequestMapper.java new file mode 100644 index 00000000..f9dae96c --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingReadRequestMapper.java @@ -0,0 +1,64 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; + +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.CollectionRoute; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.InstanceRoute; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.RelationshipCollectionRoute; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.ThingRoute; +import uk.co.compendiumdev.thingifier.application.query.ReadCollectionQuery; +import uk.co.compendiumdev.thingifier.application.query.ReadInstanceQuery; +import uk.co.compendiumdev.thingifier.application.query.ReadRelationshipQuery; +import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef; +import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; + +public final class ThingReadRequestMapper { + + private final SchemaCatalog schema; + + public ThingReadRequestMapper(final SchemaCatalog schema) { + this.schema = schema; + } + + public ThingReadRequestMapping map( + final ThingRoute route, final QueryFilterParams queryParams) { + if (route instanceof CollectionRoute) { + CollectionRoute collection = (CollectionRoute) route; + return ThingReadRequestMapping.query( + new ReadCollectionQuery(collection.entity().name(), queryParams)); + } + + if (route instanceof InstanceRoute) { + InstanceRoute instance = (InstanceRoute) route; + String identifierCandidate = instance.identifier(); + if (schema.hasRelationshipNamed(identifierCandidate) + || entityForTerm(identifierCandidate) != null) { + return notFound(route.originalPath()); + } + + return ThingReadRequestMapping.query( + new ReadInstanceQuery( + instance.entity().name(), identifierCandidate, queryParams)); + } + + if (route instanceof RelationshipCollectionRoute) { + RelationshipCollectionRoute relationship = (RelationshipCollectionRoute) route; + return ThingReadRequestMapping.query( + new ReadRelationshipQuery( + relationship.parentEntity().name(), + relationship.parentIdentifier(), + relationship.relationshipName(), + queryParams)); + } + + return notFound(route.originalPath()); + } + + private ThingReadRequestMapping notFound(final String url) { + return ThingReadRequestMapping.error( + ApiMappingError.withMessage( + 404, String.format("Could not find an instance with %s", url))); + } + + private EntityTypeRef entityForTerm(final String term) { + return schema.entityWithSingularOrPluralName(term); + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingReadRequestMapping.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingReadRequestMapping.java similarity index 92% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingReadRequestMapping.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingReadRequestMapping.java index 737ea8b4..f497888f 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingReadRequestMapping.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingReadRequestMapping.java @@ -1,4 +1,4 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; import uk.co.compendiumdev.thingifier.application.query.ThingReadQuery; diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingReadResultApiMapper.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingReadResultApiMapper.java similarity index 96% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingReadResultApiMapper.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingReadResultApiMapper.java index 2bc25dfa..0935030d 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingReadResultApiMapper.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingReadResultApiMapper.java @@ -1,4 +1,4 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; import java.util.List; import uk.co.compendiumdev.thingifier.api.response.ApiResponse; diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingWriteRequestMapper.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingWriteRequestMapper.java new file mode 100644 index 00000000..3c5ac7cb --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingWriteRequestMapper.java @@ -0,0 +1,171 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; + +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.commonerrorresponse.NoSuchEntity; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.CollectionRoute; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.InstanceRoute; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.RelationshipCollectionRoute; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.RelationshipInstanceRoute; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.ThingRoute; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.UnmatchedRoute; +import uk.co.compendiumdev.thingifier.api.http.bodyparser.ApiBodyFields; +import uk.co.compendiumdev.thingifier.application.command.DeleteThingCommand; +import uk.co.compendiumdev.thingifier.application.command.DisconnectRelationshipCommand; +import uk.co.compendiumdev.thingifier.application.command.RelateThingCommand; +import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef; +import uk.co.compendiumdev.thingifier.application.schema.RelationshipSpec; +import uk.co.compendiumdev.thingifier.application.schema.SchemaViewCatalog; + +public final class ThingWriteRequestMapper { + + private final SchemaViewCatalog schema; + private final ThingBodyCommandMapper bodyCommandMapper; + + public ThingWriteRequestMapper(final SchemaViewCatalog schema) { + this.schema = schema; + this.bodyCommandMapper = new ThingBodyCommandMapper(schema); + } + + public ThingWriteRequestMapping mapPost( + final ThingRoute route, final ApiBodyFields bodyFields) { + if (route instanceof CollectionRoute) { + CollectionRoute collection = (CollectionRoute) route; + return bodyCommandMapper.mapCreate(bodyFields, collection.entity(), true); + } + + if (route instanceof InstanceRoute) { + InstanceRoute instance = (InstanceRoute) route; + return mapPostToInstance(instance, bodyFields); + } + + if (route instanceof UnmatchedRoute) { + UnmatchedRoute unmatched = (UnmatchedRoute) route; + if (!unmatched.firstPart().isEmpty() && unmatched.partCount() == 2) { + return ThingWriteRequestMapping.error(NoSuchEntity.error(unmatched.firstPart())); + } + } + + if (route instanceof RelationshipCollectionRoute) { + RelationshipCollectionRoute relationship = (RelationshipCollectionRoute) route; + return mapPostToRelationship(relationship, bodyFields); + } + + return ThingWriteRequestMapping.error( + ApiMappingError.withMessage(400, "Your request was not understood")); + } + + public ThingWriteRequestMapping mapPut(final ThingRoute route, final ApiBodyFields bodyFields) { + if (route instanceof CollectionRoute) { + return ThingWriteRequestMapping.error( + ApiMappingError.withMessage(405, "Cannot create root level entity with a PUT")); + } + + if (route instanceof InstanceRoute) { + InstanceRoute instance = (InstanceRoute) route; + return bodyCommandMapper.mapPut(bodyFields, instance.entity(), instance.identifier()); + } + + if (route instanceof UnmatchedRoute) { + UnmatchedRoute unmatched = (UnmatchedRoute) route; + if (!unmatched.firstPart().isEmpty() && unmatched.partCount() == 2) { + return ThingWriteRequestMapping.error(NoSuchEntity.error(unmatched.firstPart())); + } + } + + return ThingWriteRequestMapping.error( + ApiMappingError.withMessage(400, "Your request was not understood")); + } + + public ThingWriteRequestMapping mapDelete(final ThingRoute route) { + if (route instanceof CollectionRoute) { + return ThingWriteRequestMapping.error( + ApiMappingError.withMessage(405, "Cannot delete root level entity")); + } + + if (route instanceof InstanceRoute) { + InstanceRoute instance = (InstanceRoute) route; + return ThingWriteRequestMapping.command( + new DeleteThingCommand(instance.entity().name(), instance.identifier()), + ApiRouteDisplay.originalPath(route.originalPath())); + } + + if (route instanceof RelationshipInstanceRoute) { + RelationshipInstanceRoute relationship = (RelationshipInstanceRoute) route; + return ThingWriteRequestMapping.command( + new DisconnectRelationshipCommand( + relationship.parentEntity().name(), + relationship.parentIdentifier(), + relationship.relationshipName(), + relationship.childIdentifier()), + ApiRouteDisplay.originalPath(route.originalPath())); + } + + return ThingWriteRequestMapping.error( + ApiMappingError.withMessage( + 404, + String.format( + "Could not find any instances with %s", route.originalPath()))); + } + + private ThingWriteRequestMapping mapPostToInstance( + final InstanceRoute route, final ApiBodyFields bodyFields) { + if (route.entity().hasPrimaryKeyField()) { + ThingWriteRequestMapping mapping = + bodyCommandMapper.mapAmend( + bodyFields, route.entity(), route.identifier(), false); + return mapping.withRouteDisplay( + ApiRouteDisplay.missingInstanceMessage( + String.format( + "No such %s entity instance with %s == %s found", + route.entity().name(), + route.entity().primaryKeyFieldName(), + route.identifier()))); + } + + return ThingWriteRequestMapping.error( + ApiMappingError.withMessage( + 404, + String.format( + "Entity %s does not have a primary key defined", + route.entity().name()))); + } + + private ThingWriteRequestMapping mapPostToRelationship( + final RelationshipCollectionRoute route, final ApiBodyFields bodyFields) { + EntityTypeRef childEntity = firstRelationshipTarget(route); + if (childEntity == null) { + return ThingWriteRequestMapping.error( + ApiMappingError.withMessage(400, "Your request was not understood")); + } + + RelationshipBodyCommands relationships = + bodyCommandMapper.parseRelationships(bodyFields, childEntity); + if (!relationships.validationReport().isValid()) { + return ThingWriteRequestMapping.error( + ApiMappingError.withMessage( + 400, + String.format( + "Invalid relationships: %s", + relationships.validationReport().getCombinedErrorMessages()))); + } + + return ThingWriteRequestMapping.command( + new RelateThingCommand( + route.parentEntity().name(), + route.parentIdentifier(), + route.relationshipName(), + bodyCommandMapper.fieldValuesExcludingRelationships( + bodyFields, relationships), + bodyCommandMapper.bodyFieldValues(bodyFields), + relationships.references()), + ApiRouteDisplay.originalPath(route.originalPath())); + } + + private EntityTypeRef firstRelationshipTarget(final RelationshipCollectionRoute route) { + for (RelationshipSpec relationship : route.parentEntity().relationships()) { + if (relationship.name().equals(route.relationshipName())) { + return schema.entityWithSingularOrPluralName(relationship.toEntityName()); + } + } + return null; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingWriteRequestMapping.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingWriteRequestMapping.java new file mode 100644 index 00000000..5682783b --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingWriteRequestMapping.java @@ -0,0 +1,52 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; + +import uk.co.compendiumdev.thingifier.application.command.ThingWriteCommand; + +public final class ThingWriteRequestMapping { + + private final ThingWriteCommand command; + private final ApiMappingError error; + private final ApiRouteDisplay routeDisplay; + + private ThingWriteRequestMapping( + final ThingWriteCommand command, + final ApiMappingError error, + final ApiRouteDisplay routeDisplay) { + this.command = command; + this.error = error; + this.routeDisplay = routeDisplay == null ? ApiRouteDisplay.empty() : routeDisplay; + } + + public static ThingWriteRequestMapping command(final ThingWriteCommand command) { + return command(command, ApiRouteDisplay.empty()); + } + + public static ThingWriteRequestMapping command( + final ThingWriteCommand command, final ApiRouteDisplay routeDisplay) { + return new ThingWriteRequestMapping(command, null, routeDisplay); + } + + public static ThingWriteRequestMapping error(final ApiMappingError error) { + return new ThingWriteRequestMapping(null, error, ApiRouteDisplay.empty()); + } + + public ThingWriteRequestMapping withRouteDisplay(final ApiRouteDisplay newRouteDisplay) { + return new ThingWriteRequestMapping(command, error, newRouteDisplay); + } + + public boolean isError() { + return error != null; + } + + public ThingWriteCommand getCommand() { + return command; + } + + public ApiMappingError getError() { + return error; + } + + public ApiRouteDisplay getRouteDisplay() { + return routeDisplay; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingifierApiRuntime.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingifierApiRuntime.java new file mode 100644 index 00000000..366d4fae --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingifierApiRuntime.java @@ -0,0 +1,23 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; + +import java.util.List; +import uk.co.compendiumdev.thingifier.api.http.ThingifierRequestContext; +import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock; +import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; +import uk.co.compendiumdev.thingifier.application.ThingCommandService; +import uk.co.compendiumdev.thingifier.application.ThingQueryService; + +public interface ThingifierApiRuntime { + + SchemaCatalog schema(); + + ThingifierApiConfig apiConfig(); + + List thingNames(); + + ThingifierRequestContext contextFrom(HttpHeadersBlock requestHeaders); + + ThingCommandService commandService(ThingifierRequestContext context); + + ThingQueryService queryService(); +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingifierSchemaCatalog.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingifierSchemaCatalog.java new file mode 100644 index 00000000..52f53f80 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingifierSchemaCatalog.java @@ -0,0 +1,76 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; + +import java.util.ArrayList; +import java.util.List; +import uk.co.compendiumdev.thingifier.Thingifier; +import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef; +import uk.co.compendiumdev.thingifier.application.schema.FieldSpec; +import uk.co.compendiumdev.thingifier.application.schema.RelationshipSpec; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; +import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipVectorDefinition; + +public final class ThingifierSchemaCatalog implements SchemaCatalog { + + private final Thingifier thingifier; + + public ThingifierSchemaCatalog(final Thingifier thingifier) { + this.thingifier = thingifier; + } + + @Override + public EntityDefinition definitionWithSingularOrPluralNamed(final String term) { + return thingifier.getERmodel().getSchema().getDefinitionWithSingularOrPluralNamed(term); + } + + @Override + public EntityDefinition entityNamed(final String name) { + return thingifier.getDefinitionNamed(name); + } + + @Override + public EntityTypeRef entityWithSingularOrPluralName(final String term) { + EntityDefinition definition = definitionWithSingularOrPluralNamed(term); + if (definition == null) { + return null; + } + return refFor(definition); + } + + @Override + public boolean hasRelationshipNamed(final String name) { + return thingifier.getERmodel().getSchema().hasRelationshipNamed(name); + } + + private EntityTypeRef refFor(final EntityDefinition definition) { + String primaryKey = + definition.hasPrimaryKeyField() ? definition.getPrimaryKeyField().getName() : ""; + return new EntityTypeRef( + definition.getName(), + definition.getPlural(), + primaryKey, + fieldsFor(definition), + relationshipsFor(definition)); + } + + private List fieldsFor(final EntityDefinition definition) { + List fields = new ArrayList<>(); + for (String fieldName : definition.getFieldNames()) { + Field field = definition.getField(fieldName); + fields.add(new FieldSpec(field.getName(), field.getType(), field.mustBeUnique())); + } + return fields; + } + + private List relationshipsFor(final EntityDefinition definition) { + List relationships = new ArrayList<>(); + for (RelationshipVectorDefinition relationship : definition.related().getRelationships()) { + relationships.add( + new RelationshipSpec( + relationship.getName(), + relationship.getFrom().getName(), + relationship.getTo().getName())); + } + return relationships; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/commonerrorresponse/NoSuchEntity.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/commonerrorresponse/NoSuchEntity.java similarity index 58% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/commonerrorresponse/NoSuchEntity.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/commonerrorresponse/NoSuchEntity.java index 4f417590..fa1a6f81 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/commonerrorresponse/NoSuchEntity.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/commonerrorresponse/NoSuchEntity.java @@ -1,6 +1,6 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers.commonerrorresponse; +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers.commonerrorresponse; -import uk.co.compendiumdev.thingifier.api.restapihandlers.ApiMappingError; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ApiMappingError; public class NoSuchEntity { diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/CollectionRoute.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/CollectionRoute.java new file mode 100644 index 00000000..580f39f2 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/CollectionRoute.java @@ -0,0 +1,17 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route; + +import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef; + +public final class CollectionRoute extends ThingRoute { + + private final EntityTypeRef entity; + + CollectionRoute(final String originalPath, final EntityTypeRef entity) { + super(originalPath); + this.entity = entity; + } + + public EntityTypeRef entity() { + return entity; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/InstanceRoute.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/InstanceRoute.java new file mode 100644 index 00000000..38c21325 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/InstanceRoute.java @@ -0,0 +1,23 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route; + +import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef; + +public final class InstanceRoute extends ThingRoute { + + private final EntityTypeRef entity; + private final String identifier; + + InstanceRoute(final String originalPath, final EntityTypeRef entity, final String identifier) { + super(originalPath); + this.entity = entity; + this.identifier = identifier; + } + + public EntityTypeRef entity() { + return entity; + } + + public String identifier() { + return identifier; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/RelationshipCollectionRoute.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/RelationshipCollectionRoute.java new file mode 100644 index 00000000..da8175c6 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/RelationshipCollectionRoute.java @@ -0,0 +1,33 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route; + +import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef; + +public final class RelationshipCollectionRoute extends ThingRoute { + + private final EntityTypeRef parentEntity; + private final String parentIdentifier; + private final String relationshipName; + + RelationshipCollectionRoute( + final String originalPath, + final EntityTypeRef parentEntity, + final String parentIdentifier, + final String relationshipName) { + super(originalPath); + this.parentEntity = parentEntity; + this.parentIdentifier = parentIdentifier; + this.relationshipName = relationshipName; + } + + public EntityTypeRef parentEntity() { + return parentEntity; + } + + public String parentIdentifier() { + return parentIdentifier; + } + + public String relationshipName() { + return relationshipName; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/RelationshipInstanceRoute.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/RelationshipInstanceRoute.java new file mode 100644 index 00000000..629d7f2b --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/RelationshipInstanceRoute.java @@ -0,0 +1,40 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route; + +import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef; + +public final class RelationshipInstanceRoute extends ThingRoute { + + private final EntityTypeRef parentEntity; + private final String parentIdentifier; + private final String relationshipName; + private final String childIdentifier; + + RelationshipInstanceRoute( + final String originalPath, + final EntityTypeRef parentEntity, + final String parentIdentifier, + final String relationshipName, + final String childIdentifier) { + super(originalPath); + this.parentEntity = parentEntity; + this.parentIdentifier = parentIdentifier; + this.relationshipName = relationshipName; + this.childIdentifier = childIdentifier; + } + + public EntityTypeRef parentEntity() { + return parentEntity; + } + + public String parentIdentifier() { + return parentIdentifier; + } + + public String relationshipName() { + return relationshipName; + } + + public String childIdentifier() { + return childIdentifier; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/ThingRoute.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/ThingRoute.java new file mode 100644 index 00000000..acbf703b --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/ThingRoute.java @@ -0,0 +1,14 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route; + +public abstract class ThingRoute { + + private final String originalPath; + + ThingRoute(final String originalPath) { + this.originalPath = originalPath; + } + + public String originalPath() { + return originalPath; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/ThingRouteMapper.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/ThingRouteMapper.java new file mode 100644 index 00000000..a45d8a9b --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/ThingRouteMapper.java @@ -0,0 +1,57 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route; + +import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef; +import uk.co.compendiumdev.thingifier.application.schema.SchemaViewCatalog; + +public final class ThingRouteMapper { + + private final SchemaViewCatalog schema; + + public ThingRouteMapper(final SchemaViewCatalog schema) { + this.schema = schema; + } + + public ThingRoute map(final String path) { + String[] parts = parts(path); + if (parts.length == 0 || parts.length > 4) { + return new UnmatchedRoute(path, parts); + } + + EntityTypeRef entity = schema.entityWithSingularOrPluralName(parts[0]); + if (entity == null) { + return new UnmatchedRoute(path, parts); + } + + if (parts.length == 1) { + return new CollectionRoute(path, entity); + } + + if (parts.length == 2) { + return new InstanceRoute(path, entity, parts[1]); + } + + if (!entity.hasRelationship(parts[2])) { + return new UnmatchedRoute(path, parts); + } + + if (parts.length == 3) { + return new RelationshipCollectionRoute(path, entity, parts[1], parts[2]); + } + + return new RelationshipInstanceRoute(path, entity, parts[1], parts[2], parts[3]); + } + + public static String[] parts(final String path) { + String normalized = path == null ? "" : path.trim(); + while (normalized.startsWith("/")) { + normalized = normalized.substring(1); + } + while (normalized.endsWith("/")) { + normalized = normalized.substring(0, normalized.length() - 1); + } + if (normalized.isEmpty()) { + return new String[0]; + } + return normalized.split("/"); + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/UnmatchedRoute.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/UnmatchedRoute.java new file mode 100644 index 00000000..3e8cc647 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/UnmatchedRoute.java @@ -0,0 +1,26 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route; + +public final class UnmatchedRoute extends ThingRoute { + + private final String[] parts; + + UnmatchedRoute(final String originalPath, final String[] parts) { + super(originalPath); + this.parts = parts.clone(); + } + + public String[] parts() { + return parts.clone(); + } + + public int partCount() { + return parts.length; + } + + public String firstPart() { + if (parts.length == 0) { + return ""; + } + return parts[0]; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/AdhocDocumentedSparkRouteConfigurer.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/AdhocDocumentedSparkRouteConfigurer.java index 79b7cb19..9054d682 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/AdhocDocumentedSparkRouteConfigurer.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/AdhocDocumentedSparkRouteConfigurer.java @@ -6,7 +6,6 @@ import uk.co.compendiumdev.thingifier.api.docgen.RoutingStatus; import uk.co.compendiumdev.thingifier.api.docgen.RoutingVerb; import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; -import uk.co.compendiumdev.thingifier.spark.SimpleSparkRouteCreator; /* The API documentation is auto generated. diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/MainImplementation.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/MainImplementation.java index 6a48fcbf..5ebfc74e 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/MainImplementation.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/MainImplementation.java @@ -5,13 +5,11 @@ import java.util.Map; import spark.Spark; import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.http.SparkHttpGenericExceptionRoutings; -import uk.co.compendiumdev.thingifier.adapter.http.ThingifierAutoDocGenRouting; -import uk.co.compendiumdev.thingifier.adapter.http.ThingifierHttpApiRoutings; -import uk.co.compendiumdev.thingifier.adapter.http.routehandlers.ShutdownRouteHandler; +import uk.co.compendiumdev.thingifier.adapter.bootstrap.ThingifierServerBootstrap; import uk.co.compendiumdev.thingifier.adapter.spark.messagehooks.ClearDataPreSparkRequestHook; import uk.co.compendiumdev.thingifier.adapter.spark.messagehooks.LogTheResponseHook; import uk.co.compendiumdev.thingifier.adapter.spark.messagehooks.LogTheSparkRequestHook; +import uk.co.compendiumdev.thingifier.adapter.spark.routehandlers.ShutdownRouteHandler; import uk.co.compendiumdev.thingifier.api.docgen.RoutingDefinition; import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfigProfile; @@ -279,15 +277,8 @@ public ThingifierHttpApiRoutings startRestServer() { throw new RuntimeException("No Thingifier Model Setup"); } - apiDefn.setThingifier(thingifier); - - // start the docs and swagger endpoints - new ThingifierAutoDocGenRouting(thingifier, apiDefn, guiManagement); - - restServer = new ThingifierHttpApiRoutings(thingifier, apiDefn); - - // sets up the wide * based generic 404s so no routings created after this will work - new SparkHttpGenericExceptionRoutings(); + restServer = + new ThingifierServerBootstrap().startRestServer(thingifier, apiDefn, guiManagement); System.out.println("Running on " + Spark.port()); System.out.println(" e.g. http://localhost:" + Spark.port()); diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/spark/SimpleSparkRouteCreator.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/SimpleSparkRouteCreator.java similarity index 98% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/spark/SimpleSparkRouteCreator.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/SimpleSparkRouteCreator.java index 0719f7e7..b95cf48d 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/spark/SimpleSparkRouteCreator.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/SimpleSparkRouteCreator.java @@ -1,4 +1,4 @@ -package uk.co.compendiumdev.thingifier.spark; +package uk.co.compendiumdev.thingifier.adapter.spark; import static spark.Spark.*; diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/spark/SparkAsText.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/SparkAsText.java similarity index 96% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/spark/SparkAsText.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/SparkAsText.java index 0f26b92a..ed03a5f9 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/spark/SparkAsText.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/SparkAsText.java @@ -1,4 +1,4 @@ -package uk.co.compendiumdev.thingifier.spark; +package uk.co.compendiumdev.thingifier.adapter.spark; import spark.Request; diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/SparkHttpGenericExceptionRoutings.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/SparkHttpGenericExceptionRoutings.java similarity index 92% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/SparkHttpGenericExceptionRoutings.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/SparkHttpGenericExceptionRoutings.java index 238078b1..b987f102 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/SparkHttpGenericExceptionRoutings.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/SparkHttpGenericExceptionRoutings.java @@ -1,11 +1,10 @@ -package uk.co.compendiumdev.thingifier.adapter.http; +package uk.co.compendiumdev.thingifier.adapter.spark; import static spark.Spark.exception; import java.util.List; import spark.Request; import uk.co.compendiumdev.thingifier.api.response.ApiResponseError; -import uk.co.compendiumdev.thingifier.spark.SimpleSparkRouteCreator; public class SparkHttpGenericExceptionRoutings { diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/ThingifierAutoDocGenRouting.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/ThingifierAutoDocGenRouting.java similarity index 98% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/ThingifierAutoDocGenRouting.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/ThingifierAutoDocGenRouting.java index 38442a6c..640cea5f 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/ThingifierAutoDocGenRouting.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/ThingifierAutoDocGenRouting.java @@ -1,4 +1,4 @@ -package uk.co.compendiumdev.thingifier.adapter.http; +package uk.co.compendiumdev.thingifier.adapter.spark; import static spark.Spark.get; diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/ThingifierHttpApiRoutings.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/ThingifierHttpApiRoutings.java similarity index 97% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/ThingifierHttpApiRoutings.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/ThingifierHttpApiRoutings.java index 5cdc3a99..d55502b9 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/ThingifierHttpApiRoutings.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/ThingifierHttpApiRoutings.java @@ -1,4 +1,4 @@ -package uk.co.compendiumdev.thingifier.adapter.http; +package uk.co.compendiumdev.thingifier.adapter.spark; import static spark.Spark.*; @@ -10,7 +10,10 @@ import uk.co.compendiumdev.thingifier.adapter.http.messagehooks.HttpApiResponseHook; import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpRequest; import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpResponse; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.*; +import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.ThingifierHttpApiBridge; +import uk.co.compendiumdev.thingifier.adapter.spark.conversion.InternalHttpResponseToSpark; +import uk.co.compendiumdev.thingifier.adapter.spark.conversion.SparkResponseToInternalHttpResponse; +import uk.co.compendiumdev.thingifier.adapter.spark.conversion.SparkToInternalHttpRequest; import uk.co.compendiumdev.thingifier.adapter.spark.messagehooks.InternalHttpRequestHook; import uk.co.compendiumdev.thingifier.adapter.spark.messagehooks.InternalHttpResponseHook; import uk.co.compendiumdev.thingifier.adapter.spark.messagehooks.SparkRequestResponseHook; @@ -18,7 +21,6 @@ import uk.co.compendiumdev.thingifier.api.docgen.ApiRoutingDefinitionDocGenerator; import uk.co.compendiumdev.thingifier.api.docgen.RoutingDefinition; import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; -import uk.co.compendiumdev.thingifier.spark.SimpleSparkRouteCreator; public class ThingifierHttpApiRoutings { diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/internalhttp/conversion/InternalHttpResponseToSpark.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/conversion/InternalHttpResponseToSpark.java similarity index 93% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/internalhttp/conversion/InternalHttpResponseToSpark.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/conversion/InternalHttpResponseToSpark.java index 32781972..43bc19ee 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/internalhttp/conversion/InternalHttpResponseToSpark.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/conversion/InternalHttpResponseToSpark.java @@ -1,4 +1,4 @@ -package uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion; +package uk.co.compendiumdev.thingifier.adapter.spark.conversion; import java.util.Map; import spark.Response; diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/internalhttp/conversion/SparkResponseToInternalHttpResponse.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/conversion/SparkResponseToInternalHttpResponse.java similarity index 92% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/internalhttp/conversion/SparkResponseToInternalHttpResponse.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/conversion/SparkResponseToInternalHttpResponse.java index e7ec338f..11866fee 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/internalhttp/conversion/SparkResponseToInternalHttpResponse.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/conversion/SparkResponseToInternalHttpResponse.java @@ -1,4 +1,4 @@ -package uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion; +package uk.co.compendiumdev.thingifier.adapter.spark.conversion; import spark.Response; import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpResponse; diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/internalhttp/conversion/SparkToInternalHttpRequest.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/conversion/SparkToInternalHttpRequest.java similarity index 96% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/internalhttp/conversion/SparkToInternalHttpRequest.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/conversion/SparkToInternalHttpRequest.java index 83492624..b1f6b6f4 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/internalhttp/conversion/SparkToInternalHttpRequest.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/conversion/SparkToInternalHttpRequest.java @@ -1,4 +1,4 @@ -package uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion; +package uk.co.compendiumdev.thingifier.adapter.spark.conversion; import java.util.ArrayList; import java.util.Enumeration; diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/routehandlers/ShutdownRouteHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/routehandlers/ShutdownRouteHandler.java similarity index 95% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/routehandlers/ShutdownRouteHandler.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/routehandlers/ShutdownRouteHandler.java index 9d3fa502..b402d180 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/routehandlers/ShutdownRouteHandler.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/routehandlers/ShutdownRouteHandler.java @@ -1,4 +1,4 @@ -package uk.co.compendiumdev.thingifier.adapter.http.routehandlers; +package uk.co.compendiumdev.thingifier.adapter.spark.routehandlers; import static spark.Spark.get; diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/routehandlers/SparkApiRequestResponseHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/routehandlers/SparkApiRequestResponseHandler.java similarity index 89% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/routehandlers/SparkApiRequestResponseHandler.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/routehandlers/SparkApiRequestResponseHandler.java index 183a6f86..d39816de 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/routehandlers/SparkApiRequestResponseHandler.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/spark/routehandlers/SparkApiRequestResponseHandler.java @@ -1,13 +1,14 @@ -package uk.co.compendiumdev.thingifier.adapter.http.routehandlers; +package uk.co.compendiumdev.thingifier.adapter.spark.routehandlers; import spark.Request; import spark.Response; import uk.co.compendiumdev.thingifier.Thingifier; +import uk.co.compendiumdev.thingifier.adapter.http.routehandlers.HttpApiRequestHandler; import uk.co.compendiumdev.thingifier.adapter.internalhttp.InternalHttpRequest; import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.HttpApiResponseToInternalHttpResponse; import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.InternalHttpRequestToHttpApiRequest; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.InternalHttpResponseToSpark; -import uk.co.compendiumdev.thingifier.adapter.internalhttp.conversion.SparkToInternalHttpRequest; +import uk.co.compendiumdev.thingifier.adapter.spark.conversion.InternalHttpResponseToSpark; +import uk.co.compendiumdev.thingifier.adapter.spark.conversion.SparkToInternalHttpRequest; import uk.co.compendiumdev.thingifier.api.ermodelconversion.JsonThing; import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; import uk.co.compendiumdev.thingifier.api.http.HttpApiResponse; diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/ThingifierRestAPIHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/ThingifierRestAPIHandler.java index d6d2a199..97c2502d 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/ThingifierRestAPIHandler.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/ThingifierRestAPIHandler.java @@ -1,7 +1,11 @@ package uk.co.compendiumdev.thingifier.api; import uk.co.compendiumdev.thingifier.Thingifier; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.DefaultThingifierApiRuntime; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingifierApiRuntime; +import uk.co.compendiumdev.thingifier.api.http.ApiRequestEnvelope; import uk.co.compendiumdev.thingifier.api.http.ThingifierRequestContext; +import uk.co.compendiumdev.thingifier.api.http.bodyparser.ApiBodyFields; import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser; import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock; import uk.co.compendiumdev.thingifier.api.response.ApiResponse; @@ -9,18 +13,22 @@ import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; public class ThingifierRestAPIHandler { - private final Thingifier thingifier; + private final ThingifierApiRuntime runtime; private final RestApiDeleteHandler delete; private final RestApiPostHandler post; private final RestApiPutHandler put; private final RestApiGetHandler get; public ThingifierRestAPIHandler(final Thingifier aThingifier) { - this.thingifier = aThingifier; - this.get = new RestApiGetHandler(aThingifier); - this.delete = new RestApiDeleteHandler(aThingifier); - this.post = new RestApiPostHandler(aThingifier); - this.put = new RestApiPutHandler(aThingifier); + this(new DefaultThingifierApiRuntime(aThingifier)); + } + + public ThingifierRestAPIHandler(final ThingifierApiRuntime runtime) { + this.runtime = runtime; + this.get = new RestApiGetHandler(runtime); + this.delete = new RestApiDeleteHandler(runtime); + this.post = new RestApiPostHandler(runtime); + this.put = new RestApiPutHandler(runtime); } // TODO: we should be able to accept xml with correct content type @@ -41,6 +49,11 @@ public ApiResponse get( return withRepository(get.handle(url, queryParams, context), context); } + public ApiResponse get(final ApiRequestEnvelope request) { + ThingifierRequestContext context = contextFrom(request.headers()); + return withRepository(get.handle(request.path(), request.queryParams(), context), context); + } + public ApiResponse head( final String url, final QueryFilterParams queryParams, HttpHeadersBlock headers) { ThingifierRequestContext context = contextFrom(headers); @@ -49,23 +62,59 @@ public ApiResponse head( return withRepository(response, context); } + public ApiResponse head(final ApiRequestEnvelope request) { + ThingifierRequestContext context = contextFrom(request.headers()); + final ApiResponse response = get.handle(request.path(), request.queryParams(), context); + response.clearBody(); + return withRepository(response, context); + } + public ApiResponse delete(final String url, HttpHeadersBlock headers) { ThingifierRequestContext context = contextFrom(headers); return withRepository(delete.handle(url, context), context); } + public ApiResponse delete(final ApiRequestEnvelope request) { + ThingifierRequestContext context = contextFrom(request.headers()); + return withRepository(delete.handle(request.path(), context), context); + } + public ApiResponse post(final String url, final BodyParser args, HttpHeadersBlock headers) { ThingifierRequestContext context = contextFrom(headers); - return withRepository(post.handle(url, args, context), context); + return post(url, args.bodyFields(), context); + } + + public ApiResponse post(final ApiRequestEnvelope request) { + ThingifierRequestContext context = contextFrom(request.headers()); + return post(request.path(), request.bodyFields(), context); + } + + public ApiResponse post( + final String url, + final ApiBodyFields bodyFields, + final ThingifierRequestContext context) { + return withRepository(post.handle(url, bodyFields, context), context); } public ApiResponse put(final String url, final BodyParser args, HttpHeadersBlock headers) { ThingifierRequestContext context = contextFrom(headers); - return withRepository(put.handle(url, args, context), context); + return put(url, args.bodyFields(), context); + } + + public ApiResponse put(final ApiRequestEnvelope request) { + ThingifierRequestContext context = contextFrom(request.headers()); + return put(request.path(), request.bodyFields(), context); + } + + public ApiResponse put( + final String url, + final ApiBodyFields bodyFields, + final ThingifierRequestContext context) { + return withRepository(put.handle(url, bodyFields, context), context); } private ThingifierRequestContext contextFrom(final HttpHeadersBlock headers) { - return ThingifierRequestContext.from(thingifier, headers); + return runtime.contextFrom(headers); } private ApiResponse withRepository( diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/ApiRequestEnvelope.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/ApiRequestEnvelope.java new file mode 100644 index 00000000..dfc382ea --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/ApiRequestEnvelope.java @@ -0,0 +1,65 @@ +package uk.co.compendiumdev.thingifier.api.http; + +import java.util.List; +import uk.co.compendiumdev.thingifier.api.http.bodyparser.ApiBodyFields; +import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser; +import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock; +import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; + +public final class ApiRequestEnvelope { + + private final ThingifierHttpApi.HttpVerb verb; + private final String path; + private final QueryFilterParams queryParams; + private final HttpHeadersBlock headers; + private final ApiBodyFields bodyFields; + + private ApiRequestEnvelope( + final ThingifierHttpApi.HttpVerb verb, + final String path, + final QueryFilterParams queryParams, + final HttpHeadersBlock headers, + final ApiBodyFields bodyFields) { + this.verb = verb; + this.path = path; + this.queryParams = queryParams; + this.headers = headers; + this.bodyFields = bodyFields; + } + + public static ApiRequestEnvelope from( + final HttpApiRequest request, + final ThingifierHttpApi.HttpVerb verb, + final List thingNames) { + ApiBodyFields bodyFields = ApiBodyFields.empty(); + if (verb == ThingifierHttpApi.HttpVerb.POST || verb == ThingifierHttpApi.HttpVerb.PUT) { + bodyFields = new BodyParser(request, thingNames).bodyFields(); + } + return new ApiRequestEnvelope( + verb, + request.getPath(), + request.getFilterableQueryParams(), + request.getHeaders(), + bodyFields); + } + + public ThingifierHttpApi.HttpVerb verb() { + return verb; + } + + public String path() { + return path; + } + + public QueryFilterParams queryParams() { + return queryParams; + } + + public HttpHeadersBlock headers() { + return headers; + } + + public ApiBodyFields bodyFields() { + return bodyFields; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/HttpApiRequest.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/HttpApiRequest.java index 769a497a..ea76ff0d 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/HttpApiRequest.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/HttpApiRequest.java @@ -4,7 +4,6 @@ import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeaderPair; import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock; import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; -import uk.co.compendiumdev.thingifier.core.query.fromurl.UrlParamParser; public final class HttpApiRequest { @@ -90,7 +89,7 @@ public String getUrlParam(final String paramKey) { } public HttpApiRequest setFilterableQueryParams(String queryString) { - filterableQueryParams = new UrlParamParser().parse(queryString); + filterableQueryParams = new UrlQueryParamParser().parse(queryString); return this; } diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/ThingifierHttpApi.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/ThingifierHttpApi.java index 177167cf..523faba2 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/ThingifierHttpApi.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/ThingifierHttpApi.java @@ -6,7 +6,6 @@ import uk.co.compendiumdev.thingifier.adapter.http.messagehooks.HttpApiRequestHook; import uk.co.compendiumdev.thingifier.adapter.http.messagehooks.HttpApiResponseHook; import uk.co.compendiumdev.thingifier.api.ermodelconversion.JsonThing; -import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser; import uk.co.compendiumdev.thingifier.api.response.ApiResponse; public final class ThingifierHttpApi { @@ -123,46 +122,24 @@ public HttpApiResponse validateRequestSyntax( public ApiResponse routeAndProcessRequest(final HttpApiRequest request, HttpVerb verb) { ApiResponse apiResponse = null; + ApiRequestEnvelope envelope = + ApiRequestEnvelope.from(request, verb, thingifier.getThingNames()); switch (verb) { case GET: - apiResponse = - thingifier - .api() - .get( - request.getPath(), - request.getFilterableQueryParams(), - request.getHeaders()); + apiResponse = thingifier.api().get(envelope); break; case HEAD: - apiResponse = - thingifier - .api() - .head( - request.getPath(), - request.getFilterableQueryParams(), - request.getHeaders()); + apiResponse = thingifier.api().head(envelope); break; case DELETE: - apiResponse = thingifier.api().delete(request.getPath(), request.getHeaders()); + apiResponse = thingifier.api().delete(envelope); break; case POST: - apiResponse = - thingifier - .api() - .post( - request.getPath(), - new BodyParser(request, thingifier.getThingNames()), - request.getHeaders()); + apiResponse = thingifier.api().post(envelope); break; case PUT: - apiResponse = - thingifier - .api() - .put( - request.getPath(), - new BodyParser(request, thingifier.getThingNames()), - request.getHeaders()); + apiResponse = thingifier.api().put(envelope); break; default: break; diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/fromurl/UrlParamParser.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/UrlQueryParamParser.java similarity index 84% rename from ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/fromurl/UrlParamParser.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/UrlQueryParamParser.java index 4d3524fc..7dd9842c 100644 --- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/fromurl/UrlParamParser.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/UrlQueryParamParser.java @@ -1,11 +1,11 @@ -package uk.co.compendiumdev.thingifier.core.query.fromurl; +package uk.co.compendiumdev.thingifier.api.http; import java.net.URLDecoder; import uk.co.compendiumdev.thingifier.core.query.FilterBy; import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; -public class UrlParamParser { - public String urlDecode(String possiblyUrlEncodedString) { +public final class UrlQueryParamParser { + public String urlDecode(final String possiblyUrlEncodedString) { String decoded = possiblyUrlEncodedString; try { @@ -18,7 +18,7 @@ public String urlDecode(String possiblyUrlEncodedString) { return decoded; } - public QueryFilterParams parse(String rawUrlParams) { + public QueryFilterParams parse(final String rawUrlParams) { QueryFilterParams filters = new QueryFilterParams(); @@ -47,7 +47,7 @@ public QueryFilterParams parse(String rawUrlParams) { return filters; } - private FilterBy parseToFilterBy(String rawParam) { + private FilterBy parseToFilterBy(final String rawParam) { String param = rawParam.trim(); String fieldName = getFieldNameFrom(param); @@ -56,7 +56,7 @@ private FilterBy parseToFilterBy(String rawParam) { return new FilterBy(fieldName, opAndValue); } - private String getFieldNameFrom(String param) { + private String getFieldNameFrom(final String param) { // for each FilterBy operator, try to find it in the string // if present, split the string there and the fieldname is to the diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/bodyparser/ApiBodyField.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/bodyparser/ApiBodyField.java new file mode 100644 index 00000000..dc7e4503 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/bodyparser/ApiBodyField.java @@ -0,0 +1,30 @@ +package uk.co.compendiumdev.thingifier.api.http.bodyparser; + +public final class ApiBodyField { + + private final String name; + private final String value; + private final String sourceType; + + public ApiBodyField(final String name, final String value) { + this(name, value, "STRING"); + } + + public ApiBodyField(final String name, final String value, final String sourceType) { + this.name = name; + this.value = value; + this.sourceType = sourceType == null ? "Something Else" : sourceType; + } + + public String name() { + return name; + } + + public String value() { + return value; + } + + public String sourceType() { + return sourceType; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/bodyparser/ApiBodyFields.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/bodyparser/ApiBodyFields.java new file mode 100644 index 00000000..407077fd --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/bodyparser/ApiBodyFields.java @@ -0,0 +1,140 @@ +package uk.co.compendiumdev.thingifier.api.http.bodyparser; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class ApiBodyFields { + + private final Map fields; + + private ApiBodyFields(final Map fields) { + this.fields = new HashMap<>(fields); + } + + public static ApiBodyFields empty() { + return new ApiBodyFields(new HashMap<>()); + } + + public static ApiBodyFields fromMap(final Map fields) { + if (fields == null) { + return empty(); + } + return new ApiBodyFields(fields); + } + + public Map asMap() { + return Collections.unmodifiableMap(fields); + } + + public Map asStringMap() { + Map stringsInMap = new HashMap<>(); + for (String key : fields.keySet()) { + Object value = fields.get(key); + + if (value instanceof Boolean) { + stringsInMap.put(key, String.valueOf(value)); + } + + if (value instanceof String) { + stringsInMap.put(key, (String) value); + } + + if (value instanceof Double) { + stringsInMap.put(key, String.valueOf(value)); + } + } + return stringsInMap; + } + + public List> asFlattenedStringMap() { + return flattenToStringMap("", fields); + } + + public List flattenedFields() { + List flattened = new ArrayList<>(); + for (Map.Entry entry : asFlattenedStringMap()) { + flattened.add(new ApiBodyField(entry.getKey(), entry.getValue())); + } + return Collections.unmodifiableList(flattened); + } + + public List topLevelFields() { + List topLevel = new ArrayList<>(); + for (Map.Entry entry : fields.entrySet()) { + topLevel.add( + new ApiBodyField( + entry.getKey(), + stringValue(entry.getValue()), + sourceTypeName(entry.getValue()))); + } + return Collections.unmodifiableList(topLevel); + } + + private List> flattenToStringMap( + final String prefixKey, final Object value) { + List> stringsInMap = new ArrayList<>(); + if (value instanceof String) { + stringsInMap.add(new AbstractMap.SimpleEntry<>(prefixKey, (String) value)); + } + if (value instanceof Double) { + stringsInMap.add(new AbstractMap.SimpleEntry<>(prefixKey, String.valueOf(value))); + } + if (value instanceof Boolean) { + stringsInMap.add(new AbstractMap.SimpleEntry<>(prefixKey, String.valueOf(value))); + } + if (value instanceof Integer) { + stringsInMap.add(new AbstractMap.SimpleEntry<>(prefixKey, String.valueOf(value))); + } + + String separator = ""; + if (prefixKey != null && !prefixKey.isEmpty() && !prefixKey.endsWith(".")) { + separator = "."; + } + if (value instanceof Map) { + for (Map.Entry entry : ((Map) value).entrySet()) { + List> nestedValues = + flattenToStringMap( + prefixKey + separator + entry.getKey(), entry.getValue()); + stringsInMap.addAll(nestedValues); + } + } + if (value instanceof ArrayList) { + for (Object nestedValue : (ArrayList) value) { + List> nestedValues = + flattenToStringMap(prefixKey + separator, nestedValue); + stringsInMap.addAll(nestedValues); + } + } + return stringsInMap; + } + + private String stringValue(final Object value) { + if (value instanceof String) { + return (String) value; + } + if (value instanceof Boolean || value instanceof Double || value instanceof Integer) { + return String.valueOf(value); + } + return ""; + } + + private String sourceTypeName(final Object value) { + if (value instanceof String) { + return "STRING"; + } + if (value instanceof Boolean) { + return "BOOLEAN"; + } + if (value instanceof Integer) { + return "INTEGER"; + } + if (value instanceof Float || value instanceof Double) { + return "NUMERIC"; + } + return "Something Else"; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/bodyparser/BodyParser.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/bodyparser/BodyParser.java index ff0ef929..102d80e7 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/bodyparser/BodyParser.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/bodyparser/BodyParser.java @@ -117,6 +117,10 @@ public Map getMap() { return args; } + public ApiBodyFields bodyFields() { + return ApiBodyFields.fromMap(getMap()); + } + /* valid if error message returned is empty */ diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyCreationValidator.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyCreationValidator.java deleted file mode 100644 index 94476498..00000000 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyCreationValidator.java +++ /dev/null @@ -1,69 +0,0 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; - -import java.util.List; -import java.util.Map; -import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.reporting.ValidationReport; -import uk.co.compendiumdev.thingifier.core.repository.EntityInstanceQuery; - -public class BodyCreationValidator { - - public ValidationReport validate( - final BodyParser bodyargs, final EntityDefinition thingDefinition) { - final ValidationReport report = new ValidationReport(); - - // on creation, we should not have any protected fields in the body i.e. id or guid - - List notAllowedToCreateWithList = - thingDefinition.getFieldNamesOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID); - final Map bodyFields = bodyargs.getMap(); - for (String fieldName : notAllowedToCreateWithList) { - if (bodyFields.containsKey(fieldName)) { - report.setValid(false); - report.addErrorMessage(String.format("Not allowed to create with %s", fieldName)); - } - } - - return report; - } - - public ValidationReport areFieldsUnique( - final BodyParser bodyargs, - final EntityDefinition thingDefinition, - final EntityInstanceQuery query, - final List uniqueFields) { - - final ValidationReport report = new ValidationReport(); - - for (Map.Entry entry : bodyargs.getFlattenedStringMap()) { - - if (uniqueFields.contains(entry.getKey())) { - String existingValue = entry.getValue(); - - if (existingValue != null && existingValue.trim().length() > 0) { - // not unique if we can find something by that field value - final EntityInstance foundInstance; - if (query == null) { - foundInstance = null; - } else { - foundInstance = - query.findByField( - thingDefinition, entry.getKey(), entry.getValue()); - } - - if (foundInstance != null) { - report.setValid(false); - report.addErrorMessage( - String.format( - "Found Existing item with %s of %s", - entry.getKey(), entry.getValue())); - } - } - } - } - return report; - } -} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/EntityUrlMatcher.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/EntityUrlMatcher.java deleted file mode 100644 index 83e8526e..00000000 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/EntityUrlMatcher.java +++ /dev/null @@ -1,70 +0,0 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; - -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.repository.ThingStore; - -final class EntityUrlMatcher { - - private EntityUrlMatcher() {} - - static EntityDefinition entityFromCollectionUrl(final SchemaCatalog schema, final String url) { - String[] parts = parts(url); - if (parts.length != 1) { - return null; - } - return schema.definitionWithSingularOrPluralNamed(parts[0]); - } - - static EntityDefinition entityFromInstanceUrl(final SchemaCatalog schema, final String url) { - String[] parts = parts(url); - if (parts.length != 2) { - return null; - } - return schema.definitionWithSingularOrPluralNamed(parts[0]); - } - - static String identifierFromInstanceUrl(final String url) { - String[] parts = parts(url); - if (parts.length != 2) { - return null; - } - return parts[1]; - } - - static String entityTermFromUrl(final String url) { - String[] parts = parts(url); - if (parts.length == 0) { - return ""; - } - return parts[0]; - } - - static boolean hasPartCount(final String url, final int count) { - return parts(url).length == count; - } - - static EntityInstance findInstanceFromUrl( - final SchemaCatalog schema, final ThingStore store, final String url) { - EntityDefinition entity = entityFromInstanceUrl(schema, url); - String identifier = identifierFromInstanceUrl(url); - if (entity == null || identifier == null) { - return null; - } - return store.entityQueries().findByQueryIdentifier(entity, identifier); - } - - static String[] parts(final String url) { - String normalized = url == null ? "" : url.trim(); - while (normalized.startsWith("/")) { - normalized = normalized.substring(1); - } - while (normalized.endsWith("/")) { - normalized = normalized.substring(0, normalized.length() - 1); - } - if (normalized.isEmpty()) { - return new String[0]; - } - return normalized.split("/"); - } -} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RepositoryBackedRelationshipUrlResolver.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RepositoryBackedRelationshipUrlResolver.java deleted file mode 100644 index 11f687e3..00000000 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RepositoryBackedRelationshipUrlResolver.java +++ /dev/null @@ -1,156 +0,0 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; - -import java.util.List; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.repository.EntityInstanceQuery; -import uk.co.compendiumdev.thingifier.core.repository.RelationshipRepository; -import uk.co.compendiumdev.thingifier.core.repository.ThingStore; - -final class RepositoryBackedRelationshipUrlResolver { - - private final SchemaCatalog schema; - private final EntityInstanceQuery query; - private final RelationshipRepository relationships; - - RepositoryBackedRelationshipUrlResolver(final SchemaCatalog schema, final ThingStore store) { - this.schema = schema; - this.query = store.entityQueries(); - this.relationships = store.relationships(); - } - - RelationshipUrlResolution resolveCollection(final String url) { - String[] parts = EntityUrlMatcher.parts(url); - if (parts.length != 3) { - return RelationshipUrlResolution.notMatched(); - } - - EntityDefinition parentEntity = entityFor(parts[0]); - if (parentEntity == null || !parentEntity.related().hasRelationship(parts[2])) { - return RelationshipUrlResolution.notMatched(); - } - - EntityInstance parent = query.findByQueryIdentifier(parentEntity, parts[1]); - - return RelationshipUrlResolution.collection(parentEntity, parent, parts[2]); - } - - RelationshipUrlResolution resolveRelationshipInstance(final String url) { - String[] parts = EntityUrlMatcher.parts(url); - if (parts.length != 4) { - return RelationshipUrlResolution.notMatched(); - } - - RelationshipUrlResolution collection = - resolveCollection(parts[0] + "/" + parts[1] + "/" + parts[2]); - if (!collection.matchedRelationshipPath() || collection.parentInstance() == null) { - return collection; - } - - EntityInstance child = null; - List relatedInstances = - relationships.listRelated( - collection.parentInstance(), collection.relationshipName()); - for (EntityInstance relatedInstance : relatedInstances) { - if (matchesQueryIdentifier(relatedInstance, parts[3])) { - child = relatedInstance; - break; - } - } - - return RelationshipUrlResolution.relationshipInstance( - collection.parentEntity(), - collection.parentInstance(), - collection.relationshipName(), - child); - } - - private EntityDefinition entityFor(final String term) { - return schema.definitionWithSingularOrPluralNamed(term); - } - - private boolean matchesQueryIdentifier(final EntityInstance instance, final String identifier) { - for (Field autoIncrementField : - instance.getEntity().getFieldsOfType(FieldType.AUTO_INCREMENT)) { - String idValue = instance.getFieldValue(autoIncrementField.getName()).asString(); - if (idValue.contentEquals(identifier)) { - return true; - } - break; - } - - String primaryKeyValue = instance.getPrimaryKeyValue(); - return primaryKeyValue != null && primaryKeyValue.contentEquals(identifier); - } - - static final class RelationshipUrlResolution { - private final boolean matchedRelationshipPath; - private final boolean relationshipInstancePath; - private final EntityDefinition parentEntity; - private final EntityInstance parentInstance; - private final String relationshipName; - private final EntityInstance childInstance; - - private RelationshipUrlResolution( - final boolean matchedRelationshipPath, - final boolean relationshipInstancePath, - final EntityDefinition parentEntity, - final EntityInstance parentInstance, - final String relationshipName, - final EntityInstance childInstance) { - this.matchedRelationshipPath = matchedRelationshipPath; - this.relationshipInstancePath = relationshipInstancePath; - this.parentEntity = parentEntity; - this.parentInstance = parentInstance; - this.relationshipName = relationshipName; - this.childInstance = childInstance; - } - - static RelationshipUrlResolution notMatched() { - return new RelationshipUrlResolution(false, false, null, null, null, null); - } - - static RelationshipUrlResolution collection( - final EntityDefinition parentEntity, - final EntityInstance parentInstance, - final String relationshipName) { - return new RelationshipUrlResolution( - true, false, parentEntity, parentInstance, relationshipName, null); - } - - static RelationshipUrlResolution relationshipInstance( - final EntityDefinition parentEntity, - final EntityInstance parentInstance, - final String relationshipName, - final EntityInstance childInstance) { - return new RelationshipUrlResolution( - true, true, parentEntity, parentInstance, relationshipName, childInstance); - } - - boolean matchedRelationshipPath() { - return matchedRelationshipPath; - } - - boolean relationshipInstancePath() { - return relationshipInstancePath; - } - - EntityDefinition parentEntity() { - return parentEntity; - } - - EntityInstance parentInstance() { - return parentInstance; - } - - String relationshipName() { - return relationshipName; - } - - EntityInstance childInstance() { - return childInstance; - } - } -} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiDeleteHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiDeleteHandler.java index 60901ae6..4f8c10dd 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiDeleteHandler.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiDeleteHandler.java @@ -1,34 +1,43 @@ package uk.co.compendiumdev.thingifier.api.restapihandlers; import uk.co.compendiumdev.thingifier.Thingifier; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.DefaultThingifierApiRuntime; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingCommandResultApiMapper; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingWriteRequestMapper; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingWriteRequestMapping; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingifierApiRuntime; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.ThingRouteMapper; import uk.co.compendiumdev.thingifier.api.http.ThingifierRequestContext; import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock; import uk.co.compendiumdev.thingifier.api.response.ApiResponse; import uk.co.compendiumdev.thingifier.application.ThingCommandResult; -import uk.co.compendiumdev.thingifier.application.ThingCommandService; public class RestApiDeleteHandler { - private final Thingifier thingifier; + private final ThingifierApiRuntime runtime; public RestApiDeleteHandler(final Thingifier aThingifier) { - this.thingifier = aThingifier; + this(new DefaultThingifierApiRuntime(aThingifier)); + } + + public RestApiDeleteHandler(final ThingifierApiRuntime runtime) { + this.runtime = runtime; } public ApiResponse handle(final String url, HttpHeadersBlock requestHeaders) { - return handle(url, ThingifierRequestContext.from(thingifier, requestHeaders)); + return handle(url, runtime.contextFrom(requestHeaders)); } public ApiResponse handle(final String url, final ThingifierRequestContext context) { ThingWriteRequestMapping mapping = - new ThingWriteRequestMapper(thingifier, context.store()).mapDelete(url); + new ThingWriteRequestMapper(runtime.schema()) + .mapDelete(new ThingRouteMapper(runtime.schema()).map(url)); ThingCommandResultApiMapper apiMapper = - new ThingCommandResultApiMapper(thingifier.apiConfig()); + new ThingCommandResultApiMapper(runtime.apiConfig()); if (mapping.isError()) { return apiMapper.map(mapping.getError()); } - ThingCommandResult result = - new ThingCommandService(context.store()).execute(mapping.getCommand()); - return apiMapper.map(mapping.getCommand(), result); + ThingCommandResult result = runtime.commandService(context).execute(mapping.getCommand()); + return apiMapper.map(mapping, result); } } diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiGetHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiGetHandler.java index b27f03de..152ad09b 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiGetHandler.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiGetHandler.java @@ -1,38 +1,46 @@ package uk.co.compendiumdev.thingifier.api.restapihandlers; import uk.co.compendiumdev.thingifier.Thingifier; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ApiMappingError; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.DefaultThingifierApiRuntime; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingReadRequestMapper; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingReadRequestMapping; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingReadResultApiMapper; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingifierApiRuntime; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.ThingRouteMapper; import uk.co.compendiumdev.thingifier.api.http.ThingifierRequestContext; import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock; import uk.co.compendiumdev.thingifier.api.response.ApiResponse; -import uk.co.compendiumdev.thingifier.application.ThingQueryService; import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; import uk.co.compendiumdev.thingifier.core.query.RepositoryQueryResult; public class RestApiGetHandler { - private final Thingifier thingifier; - private final ThingQueryService queryService; + private final ThingifierApiRuntime runtime; public RestApiGetHandler(final Thingifier aThingifier) { - this.thingifier = aThingifier; - this.queryService = new ThingQueryService(); + this(new DefaultThingifierApiRuntime(aThingifier)); + } + + public RestApiGetHandler(final ThingifierApiRuntime runtime) { + this.runtime = runtime; } public ApiResponse handle( final String url, final QueryFilterParams queryParams, final HttpHeadersBlock requestHeaders) { - return handle(url, queryParams, ThingifierRequestContext.from(thingifier, requestHeaders)); + return handle(url, queryParams, runtime.contextFrom(requestHeaders)); } public ApiResponse handle( final String url, final QueryFilterParams queryParams, final ThingifierRequestContext context) { - ThingReadResultApiMapper apiMapper = new ThingReadResultApiMapper(thingifier.apiConfig()); + ThingReadResultApiMapper apiMapper = new ThingReadResultApiMapper(runtime.apiConfig()); // if there are params, and we are not allowed to filter, and we enforce that if (queryParams.size() > 0 - && thingifier.apiConfig().forParams().willEnforceFilteringThroughUrlParams() - && !thingifier.apiConfig().forParams().willAllowFilteringThroughUrlParams()) { + && runtime.apiConfig().forParams().willEnforceFilteringThroughUrlParams() + && !runtime.apiConfig().forParams().willAllowFilteringThroughUrlParams()) { return apiMapper.map( ApiMappingError.withMessage( 400, String.format("Can not use query parameters with %s", url))); @@ -40,17 +48,18 @@ public ApiResponse handle( RepositoryQueryResult queryResults; boolean allowFiltering = - thingifier.apiConfig().forParams().willAllowFilteringThroughUrlParams(); + runtime.apiConfig().forParams().willAllowFilteringThroughUrlParams(); QueryFilterParams effectiveQueryParams = allowFiltering ? queryParams : new QueryFilterParams(); ThingReadRequestMapping mapping = - new ThingReadRequestMapper(thingifier).map(url, effectiveQueryParams); + new ThingReadRequestMapper(runtime.schema()) + .map(new ThingRouteMapper(runtime.schema()).map(url), effectiveQueryParams); if (mapping.isError()) { return apiMapper.map(mapping.getError()); } - queryResults = queryService.execute(mapping.getQuery(), context.store()); + queryResults = runtime.queryService().execute(mapping.getQuery(), context.store()); return apiMapper.map(url, queryResults); } diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPostHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPostHandler.java index ed2efc74..1ed1b372 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPostHandler.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPostHandler.java @@ -1,37 +1,54 @@ package uk.co.compendiumdev.thingifier.api.restapihandlers; import uk.co.compendiumdev.thingifier.Thingifier; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.DefaultThingifierApiRuntime; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingCommandResultApiMapper; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingWriteRequestMapper; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingWriteRequestMapping; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingifierApiRuntime; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.ThingRouteMapper; import uk.co.compendiumdev.thingifier.api.http.ThingifierRequestContext; +import uk.co.compendiumdev.thingifier.api.http.bodyparser.ApiBodyFields; import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser; import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock; import uk.co.compendiumdev.thingifier.api.response.ApiResponse; import uk.co.compendiumdev.thingifier.application.ThingCommandResult; -import uk.co.compendiumdev.thingifier.application.ThingCommandService; public class RestApiPostHandler { - private final Thingifier thingifier; + private final ThingifierApiRuntime runtime; public RestApiPostHandler(final Thingifier aThingifier) { - thingifier = aThingifier; + this(new DefaultThingifierApiRuntime(aThingifier)); + } + + public RestApiPostHandler(final ThingifierApiRuntime runtime) { + this.runtime = runtime; } public ApiResponse handle( final String url, final BodyParser args, final HttpHeadersBlock requestHeaders) { - return handle(url, args, ThingifierRequestContext.from(thingifier, requestHeaders)); + return handle(url, args.bodyFields(), runtime.contextFrom(requestHeaders)); } public ApiResponse handle( final String url, final BodyParser args, final ThingifierRequestContext context) { + return handle(url, args.bodyFields(), context); + } + + public ApiResponse handle( + final String url, + final ApiBodyFields bodyFields, + final ThingifierRequestContext context) { ThingWriteRequestMapping mapping = - new ThingWriteRequestMapper(thingifier, context.store()).mapPost(url, args); + new ThingWriteRequestMapper(runtime.schema()) + .mapPost(new ThingRouteMapper(runtime.schema()).map(url), bodyFields); ThingCommandResultApiMapper apiMapper = - new ThingCommandResultApiMapper(thingifier.apiConfig()); + new ThingCommandResultApiMapper(runtime.apiConfig()); if (mapping.isError()) { return apiMapper.map(mapping.getError()); } - ThingCommandResult result = - new ThingCommandService(context.store()).execute(mapping.getCommand()); - return apiMapper.map(mapping.getCommand(), result); + ThingCommandResult result = runtime.commandService(context).execute(mapping.getCommand()); + return apiMapper.map(mapping, result); } } diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPutHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPutHandler.java index 98b326ab..d393327a 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPutHandler.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPutHandler.java @@ -1,37 +1,54 @@ package uk.co.compendiumdev.thingifier.api.restapihandlers; import uk.co.compendiumdev.thingifier.Thingifier; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.DefaultThingifierApiRuntime; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingCommandResultApiMapper; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingWriteRequestMapper; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingWriteRequestMapping; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingifierApiRuntime; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.ThingRouteMapper; import uk.co.compendiumdev.thingifier.api.http.ThingifierRequestContext; +import uk.co.compendiumdev.thingifier.api.http.bodyparser.ApiBodyFields; import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser; import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock; import uk.co.compendiumdev.thingifier.api.response.ApiResponse; import uk.co.compendiumdev.thingifier.application.ThingCommandResult; -import uk.co.compendiumdev.thingifier.application.ThingCommandService; public class RestApiPutHandler { - final Thingifier thingifier; + private final ThingifierApiRuntime runtime; public RestApiPutHandler(final Thingifier aThingifier) { - thingifier = aThingifier; + this(new DefaultThingifierApiRuntime(aThingifier)); + } + + public RestApiPutHandler(final ThingifierApiRuntime runtime) { + this.runtime = runtime; } public ApiResponse handle( final String url, final BodyParser args, final HttpHeadersBlock requestHeaders) { - return handle(url, args, ThingifierRequestContext.from(thingifier, requestHeaders)); + return handle(url, args.bodyFields(), runtime.contextFrom(requestHeaders)); } public ApiResponse handle( final String url, final BodyParser args, final ThingifierRequestContext context) { + return handle(url, args.bodyFields(), context); + } + + public ApiResponse handle( + final String url, + final ApiBodyFields bodyFields, + final ThingifierRequestContext context) { ThingWriteRequestMapping mapping = - new ThingWriteRequestMapper(thingifier, context.store()).mapPut(url, args); + new ThingWriteRequestMapper(runtime.schema()) + .mapPut(new ThingRouteMapper(runtime.schema()).map(url), bodyFields); ThingCommandResultApiMapper apiMapper = - new ThingCommandResultApiMapper(thingifier.apiConfig()); + new ThingCommandResultApiMapper(runtime.apiConfig()); if (mapping.isError()) { return apiMapper.map(mapping.getError()); } - ThingCommandResult result = - new ThingCommandService(context.store()).execute(mapping.getCommand()); - return apiMapper.map(mapping.getCommand(), result); + ThingCommandResult result = runtime.commandService(context).execute(mapping.getCommand()); + return apiMapper.map(mapping, result); } } diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/SchemaCatalog.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/SchemaCatalog.java deleted file mode 100644 index 10d22aed..00000000 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/SchemaCatalog.java +++ /dev/null @@ -1,10 +0,0 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; - -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; - -public interface SchemaCatalog { - - EntityDefinition definitionWithSingularOrPluralNamed(String term); - - boolean hasRelationshipNamed(String name); -} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingBodyCommandMapper.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingBodyCommandMapper.java deleted file mode 100644 index 26adc794..00000000 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingBodyCommandMapper.java +++ /dev/null @@ -1,254 +0,0 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser; -import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; -import uk.co.compendiumdev.thingifier.application.command.AmendThingCommand; -import uk.co.compendiumdev.thingifier.application.command.CreateAndConnectRelationshipCommand; -import uk.co.compendiumdev.thingifier.application.command.CreateThingCommand; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; -import uk.co.compendiumdev.thingifier.core.reporting.ValidationReport; -import uk.co.compendiumdev.thingifier.core.repository.ThingStore; - -public final class ThingBodyCommandMapper { - - private final SchemaCatalog schema; - private final ThingifierApiConfig apiConfig; - private final ThingStore store; - - public ThingBodyCommandMapper(final Thingifier thingifier, final String database) { - this(thingifier, thingifier.getStore(database)); - } - - ThingBodyCommandMapper(final Thingifier thingifier, final ThingStore store) { - this(new ThingifierSchemaCatalog(thingifier), thingifier.apiConfig(), store); - } - - ThingBodyCommandMapper( - final SchemaCatalog schema, - final ThingifierApiConfig apiConfig, - final ThingStore store) { - this.schema = schema; - this.apiConfig = apiConfig; - this.store = store; - } - - public ThingWriteRequestMapping mapCreate( - final BodyParser bodyargs, - final EntityDefinition entity, - final boolean validateFinalRelationships) { - RelationshipBodyCommands relationships = parseRelationships(bodyargs, entity); - if (!relationships.validationReport().isValid()) { - return invalidRelationships(relationships.validationReport()); - } - - ValidationReport validated = new BodyCreationValidator().validate(bodyargs, entity); - if (!validated.isValid()) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 400, - String.format( - "Invalid Creation: %s", validated.getCombinedErrorMessages()))); - } - - return mapCreateFromValidatedBody( - bodyargs, - entity, - EntityInstanceDraft.forEntity(entity), - relationships, - validateFinalRelationships); - } - - public ThingWriteRequestMapping mapCreateWithPrimaryKey( - final String primaryKey, final BodyParser bodyargs, final EntityDefinition entity) { - ValidationReport validated = - new BodyCreationValidator() - .areFieldsUnique( - bodyargs, - entity, - store.entityQueries(), - entity.getFieldNamesOfType( - FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID)); - if (!validated.isValid()) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 409, - "Cannot Create with duplicate values: " - + validated.getCombinedErrorMessages())); - } - - EntityInstanceDraft baseDraft = EntityInstanceDraft.forEntity(entity); - if (isProtectedField(entity.getPrimaryKeyField().getType())) { - baseDraft.withProtectedField(entity.getPrimaryKeyField().getName(), primaryKey); - } else { - baseDraft.withField(entity.getPrimaryKeyField().getName(), primaryKey); - } - - RelationshipBodyCommands relationships = parseRelationships(bodyargs, entity); - if (!relationships.validationReport().isValid()) { - return invalidRelationships(relationships.validationReport()); - } - - List fieldValues = - FieldValues.fromListMapEntryStringString(bodyargs.getFlattenedStringMap()); - store.administration().accommodateProtectedIds(entity, fieldValues); - - if (apiConfig.willApiEnforceDeclaredTypesInInput()) { - ValidationReport validatedTypes = bodyargs.validateAgainstType(entity); - if (!validatedTypes.isValid()) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 400, validatedTypes.getCombinedErrorMessages())); - } - } - - try { - List ignoreFields = entity.getFieldNamesOfType(FieldType.AUTO_GUID); - EntityInstanceDraft draft = - new EntityInstanceBulkUpdater(entity) - .overrideFieldValuesFromArgsIgnoring( - FieldValues.fromListMapEntryStringString( - fieldValuesExcludingRelationships( - bodyargs, relationships)), - ignoreFields); - copyBaseDraftValues(baseDraft, draft); - return ThingWriteRequestMapping.command( - new CreateThingCommand(draft, relationships.references(), true)); - } catch (Exception e) { - return ThingWriteRequestMapping.error(ApiMappingError.withMessage(400, e.getMessage())); - } - } - - public ThingWriteRequestMapping mapAmend( - final BodyParser bodyargs, - final EntityInstance instance, - final boolean replaceExistingFieldsAndRelationships) { - bodyargs.getMap(); - - if (apiConfig.willApiEnforceDeclaredTypesInInput()) { - List doNotValidateFields = - instance.getEntity() - .getFieldNamesOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID); - ValidationReport validatedTypes = - bodyargs.validateAgainstTypeIgnoring(instance.getEntity(), doNotValidateFields); - if (!validatedTypes.isValid()) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 400, validatedTypes.getCombinedErrorMessages())); - } - } - - RelationshipBodyCommands relationships = parseRelationships(bodyargs, instance.getEntity()); - EntityInstanceDraft draft; - try { - List fieldValues = - FieldValues.fromListMapEntryStringString( - fieldValuesExcludingRelationships(bodyargs, relationships)); - draft = new EntityInstanceBulkUpdater(instance).setFieldValuesFrom(fieldValues); - } catch (Exception e) { - return ThingWriteRequestMapping.error(ApiMappingError.withMessage(400, e.getMessage())); - } - - if (!relationships.validationReport().isValid()) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessages( - 400, relationships.validationReport().getErrorMessages())); - } - - return ThingWriteRequestMapping.command( - new AmendThingCommand( - instance, - draft, - replaceExistingFieldsAndRelationships, - relationships.references())); - } - - public ThingWriteRequestMapping mapCreateAndConnect( - final BodyParser bodyargs, - final EntityInstance parent, - final String relationshipName, - final EntityDefinition childEntity) { - ThingWriteRequestMapping createMapping = mapCreate(bodyargs, childEntity, false); - if (createMapping.isError()) { - return createMapping; - } - - CreateThingCommand create = (CreateThingCommand) createMapping.getCommand(); - return ThingWriteRequestMapping.command( - new CreateAndConnectRelationshipCommand( - parent, relationshipName, create.getDraft(), create.getRelationships())); - } - - private ThingWriteRequestMapping mapCreateFromValidatedBody( - final BodyParser bodyargs, - final EntityDefinition entity, - final EntityInstanceDraft baseDraft, - final RelationshipBodyCommands relationships, - final boolean validateFinalRelationships) { - if (apiConfig.willApiEnforceDeclaredTypesInInput()) { - ValidationReport validatedTypes = bodyargs.validateAgainstType(entity); - if (!validatedTypes.isValid()) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 400, validatedTypes.getCombinedErrorMessages())); - } - } - - try { - List fieldValues = - FieldValues.fromListMapEntryStringString( - fieldValuesExcludingRelationships(bodyargs, relationships)); - EntityInstanceDraft draft = - new EntityInstanceBulkUpdater(entity).setFieldValuesFrom(fieldValues); - copyBaseDraftValues(baseDraft, draft); - return ThingWriteRequestMapping.command( - new CreateThingCommand( - draft, relationships.references(), validateFinalRelationships)); - } catch (Exception e) { - return ThingWriteRequestMapping.error(ApiMappingError.withMessage(400, e.getMessage())); - } - } - - private RelationshipBodyCommands parseRelationships( - final BodyParser bodyargs, final EntityDefinition entity) { - return new RelationshipBodyCommandParser(schema).parse(bodyargs, entity); - } - - private ThingWriteRequestMapping invalidRelationships(final ValidationReport validation) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 400, - String.format( - "Invalid relationships: %s", - validation.getCombinedErrorMessages()))); - } - - private List> fieldValuesExcludingRelationships( - final BodyParser bodyargs, final RelationshipBodyCommands relationships) { - List> flattenedArgs = - new ArrayList<>(bodyargs.getFlattenedStringMap()); - flattenedArgs.removeAll(relationships.relationshipEntries()); - return flattenedArgs; - } - - private void copyBaseDraftValues( - final EntityInstanceDraft baseDraft, final EntityInstanceDraft draft) { - for (NamedValue value : baseDraft.getFieldValues()) { - draft.withField(value.getName(), value.asString()); - } - for (NamedValue protectedValue : baseDraft.getProtectedFieldValues()) { - draft.withProtectedField(protectedValue.getName(), protectedValue.asString()); - } - } - - private boolean isProtectedField(final FieldType fieldType) { - return fieldType == FieldType.AUTO_INCREMENT || fieldType == FieldType.AUTO_GUID; - } -} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingCommandResultApiMapper.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingCommandResultApiMapper.java deleted file mode 100644 index 0d49f7b0..00000000 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingCommandResultApiMapper.java +++ /dev/null @@ -1,85 +0,0 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; - -import uk.co.compendiumdev.thingifier.api.response.ApiResponse; -import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; -import uk.co.compendiumdev.thingifier.application.ThingCommandResult; -import uk.co.compendiumdev.thingifier.application.command.AmendThingCommand; -import uk.co.compendiumdev.thingifier.application.command.ConnectExistingRelationshipCommand; -import uk.co.compendiumdev.thingifier.application.command.CreateAndConnectRelationshipCommand; -import uk.co.compendiumdev.thingifier.application.command.CreateThingCommand; -import uk.co.compendiumdev.thingifier.application.command.DeleteThingCommand; -import uk.co.compendiumdev.thingifier.application.command.DisconnectRelationshipCommand; -import uk.co.compendiumdev.thingifier.application.command.ThingWriteCommand; - -public final class ThingCommandResultApiMapper { - - private static final String CREATED_ITEM_ROLLED_BACK_MESSAGE = - " the newly created item was deleted. No new items have been created."; - - private final ThingifierApiConfig apiConfig; - - public ThingCommandResultApiMapper(final ThingifierApiConfig apiConfig) { - this.apiConfig = apiConfig; - } - - public ApiResponse map(final ApiMappingError error) { - return ApiResponse.error(error.statusCode(), error.messages()); - } - - public ApiResponse map(final ThingWriteCommand command, final ThingCommandResult result) { - if (result.isError()) { - return errorResponseFor(command, result); - } - - if (command instanceof CreateThingCommand - || command instanceof CreateAndConnectRelationshipCommand) { - return ApiResponse.created(result.getInstance(), apiConfig); - } - - if (command instanceof ConnectExistingRelationshipCommand) { - return ApiResponse.created(null, apiConfig); - } - - if (command instanceof AmendThingCommand) { - return ApiResponse.success().returnSingleInstance(result.getInstance()); - } - - if (command instanceof DeleteThingCommand - || command instanceof DisconnectRelationshipCommand) { - return ApiResponse.success(); - } - - return ApiResponse.error(400, result.getErrorMessages()); - } - - private ApiResponse errorResponseFor( - final ThingWriteCommand command, final ThingCommandResult result) { - ApiResponse response; - if (command instanceof CreateThingCommand && result.getErrorMessages().size() == 1) { - response = - ApiResponse.error(400, creationErrorMessage(result.getCombinedErrorMessage())); - } else { - response = ApiResponse.error(400, result.getErrorMessages()); - } - - if (result.rolledBackCreatedInstance()) { - response.addToErrorMessages(CREATED_ITEM_ROLLED_BACK_MESSAGE); - } - return response; - } - - private String creationErrorMessage(final String rawMessage) { - String message = rawMessage; - if (message == null) { - return ""; - } - - String validationPrefix = "Failed Validation: "; - if (message.startsWith(validationPrefix) - && message.substring(validationPrefix.length()).endsWith(" : field is mandatory") - && !message.substring(validationPrefix.length()).contains(", ")) { - return message.substring(validationPrefix.length()); - } - return message; - } -} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingReadRequestMapper.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingReadRequestMapper.java deleted file mode 100644 index 90bab7c9..00000000 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingReadRequestMapper.java +++ /dev/null @@ -1,65 +0,0 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; - -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.application.query.ReadCollectionQuery; -import uk.co.compendiumdev.thingifier.application.query.ReadInstanceQuery; -import uk.co.compendiumdev.thingifier.application.query.ReadRelationshipQuery; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; - -public final class ThingReadRequestMapper { - - private final SchemaCatalog schema; - - public ThingReadRequestMapper(final Thingifier thingifier) { - this(new ThingifierSchemaCatalog(thingifier)); - } - - ThingReadRequestMapper(final SchemaCatalog schema) { - this.schema = schema; - } - - public ThingReadRequestMapping map(final String url, final QueryFilterParams queryParams) { - String[] terms = EntityUrlMatcher.parts(url); - if (terms.length == 0 || terms.length > 3) { - return notFound(url); - } - - EntityDefinition entity = entityForTerm(terms[0]); - if (entity == null) { - return notFound(url); - } - - if (terms.length == 1) { - return ThingReadRequestMapping.query(new ReadCollectionQuery(entity, queryParams)); - } - - String identifierCandidate = terms[1]; - if (schema.hasRelationshipNamed(identifierCandidate) - || entityForTerm(identifierCandidate) != null) { - return notFound(url); - } - - if (terms.length == 2) { - return ThingReadRequestMapping.query( - new ReadInstanceQuery(entity, identifierCandidate, queryParams)); - } - - if (!entity.related().hasRelationship(terms[2])) { - return notFound(url); - } - - return ThingReadRequestMapping.query( - new ReadRelationshipQuery(entity, identifierCandidate, terms[2], queryParams)); - } - - private ThingReadRequestMapping notFound(final String url) { - return ThingReadRequestMapping.error( - ApiMappingError.withMessage( - 404, String.format("Could not find an instance with %s", url))); - } - - private EntityDefinition entityForTerm(final String term) { - return schema.definitionWithSingularOrPluralNamed(term); - } -} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingWriteRequestMapper.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingWriteRequestMapper.java deleted file mode 100644 index f893070c..00000000 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingWriteRequestMapper.java +++ /dev/null @@ -1,312 +0,0 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; - -import java.util.List; -import java.util.Map; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser; -import uk.co.compendiumdev.thingifier.api.restapihandlers.commonerrorresponse.NoSuchEntity; -import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; -import uk.co.compendiumdev.thingifier.application.command.ConnectExistingRelationshipCommand; -import uk.co.compendiumdev.thingifier.application.command.DeleteThingCommand; -import uk.co.compendiumdev.thingifier.application.command.DisconnectRelationshipCommand; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; -import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipVectorDefinition; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.repository.ThingStore; - -public final class ThingWriteRequestMapper { - - private final SchemaCatalog schema; - private final ThingStore store; - private final ThingBodyCommandMapper bodyCommandMapper; - - public ThingWriteRequestMapper(final Thingifier thingifier, final String database) { - this(thingifier, thingifier.getStore(database)); - } - - public ThingWriteRequestMapper(final Thingifier thingifier, final ThingStore store) { - this(new ThingifierSchemaCatalog(thingifier), thingifier.apiConfig(), store); - } - - ThingWriteRequestMapper( - final SchemaCatalog schema, - final ThingifierApiConfig apiConfig, - final ThingStore store) { - this.schema = schema; - this.store = store; - this.bodyCommandMapper = new ThingBodyCommandMapper(schema, apiConfig, store); - } - - public ThingWriteRequestMapping mapPost(final String url, final BodyParser args) { - EntityDefinition entityDefinition = EntityUrlMatcher.entityFromCollectionUrl(schema, url); - if (entityDefinition != null) { - return bodyCommandMapper.mapCreate(args, entityDefinition, true); - } - - entityDefinition = EntityUrlMatcher.entityFromInstanceUrl(schema, url); - if (entityDefinition != null) { - return mapPostToInstance(url, args, entityDefinition); - } - - String thingName = EntityUrlMatcher.entityTermFromUrl(url); - if (!thingName.isEmpty() && EntityUrlMatcher.hasPartCount(url, 2)) { - return ThingWriteRequestMapping.error(NoSuchEntity.error(thingName)); - } - - RepositoryBackedRelationshipUrlResolver.RelationshipUrlResolution relationship = - new RepositoryBackedRelationshipUrlResolver(schema, store).resolveCollection(url); - if (relationship.matchedRelationshipPath()) { - return mapPostToRelationship(url, args, relationship); - } - - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage(400, "Your request was not understood")); - } - - public ThingWriteRequestMapping mapPut(final String url, final BodyParser args) { - EntityDefinition thing = EntityUrlMatcher.entityFromCollectionUrl(schema, url); - if (thing != null) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage(405, "Cannot create root level entity with a PUT")); - } - - thing = EntityUrlMatcher.entityFromInstanceUrl(schema, url); - if (thing == null) { - if (EntityUrlMatcher.hasPartCount(url, 2)) { - String thingName = EntityUrlMatcher.entityTermFromUrl(url); - if (!thingName.isEmpty()) { - return ThingWriteRequestMapping.error(NoSuchEntity.error(thingName)); - } - } - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage(400, "Your request was not understood")); - } - - String instanceGuid = EntityUrlMatcher.identifierFromInstanceUrl(url); - EntityInstance instance = store.entityQueries().findByQueryIdentifier(thing, instanceGuid); - - if (instance == null) { - return mapPutCreate(args, thing, instanceGuid); - } - - return bodyCommandMapper.mapAmend(args, instance, true); - } - - public ThingWriteRequestMapping mapDelete(final String url) { - EntityDefinition thing = EntityUrlMatcher.entityFromCollectionUrl(schema, url); - if (thing != null) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage(405, "Cannot delete root level entity")); - } - - EntityDefinition entity = EntityUrlMatcher.entityFromInstanceUrl(schema, url); - if (entity != null) { - EntityInstance instance = EntityUrlMatcher.findInstanceFromUrl(schema, store, url); - if (instance == null) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 404, String.format("Could not find any instances with %s", url))); - } - return ThingWriteRequestMapping.command(new DeleteThingCommand(instance)); - } - - RepositoryBackedRelationshipUrlResolver.RelationshipUrlResolution relationship = - new RepositoryBackedRelationshipUrlResolver(schema, store) - .resolveRelationshipInstance(url); - if (relationship.matchedRelationshipPath()) { - if (!relationship.relationshipInstancePath() - || relationship.parentInstance() == null - || relationship.childInstance() == null) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 404, String.format("Could not find any instances with %s", url))); - } - return ThingWriteRequestMapping.command( - new DisconnectRelationshipCommand( - relationship.parentInstance(), - relationship.childInstance(), - relationship.relationshipName())); - } - - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 404, String.format("Could not find any instances with %s", url))); - } - - private ThingWriteRequestMapping mapPostToInstance( - final String url, final BodyParser args, final EntityDefinition entityDefinition) { - String primaryKey = EntityUrlMatcher.identifierFromInstanceUrl(url); - - if (entityDefinition.hasPrimaryKeyField()) { - EntityInstance instance = - store.entityQueries().findByQueryIdentifier(entityDefinition, primaryKey); - - if (instance == null) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 404, - String.format( - "No such %s entity instance with %s == %s found", - entityDefinition.getName(), - entityDefinition.getPrimaryKeyField().getName(), - primaryKey))); - } - - return bodyCommandMapper.mapAmend(args, instance, false); - } - - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 404, - String.format( - "Entity %s does not have a primary key defined", - entityDefinition.getName()))); - } - - private ThingWriteRequestMapping mapPutCreate( - final BodyParser args, final EntityDefinition thing, final String instanceGuid) { - List forbiddenPutCreationFields = - thing.getFieldsOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID); - if (!forbiddenPutCreationFields.isEmpty()) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 400, - String.format( - "Cannot create %s with PUT due to Auto fields %s", - thing.getName(), fieldNames(forbiddenPutCreationFields)))); - } - - List fieldValues = - FieldValues.fromListMapEntryStringString(args.getFlattenedStringMap()); - for (NamedValue namedValue : fieldValues) { - if (namedValue.name.equals(thing.getPrimaryKeyField().getName()) - && !namedValue.value.equals(instanceGuid)) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 400, - String.format( - "Cannot create %s with PUT as key does not match body value %s != %s", - thing.getName(), instanceGuid, namedValue.value))); - } - } - - return bodyCommandMapper.mapCreateWithPrimaryKey(instanceGuid, args, thing); - } - - private ThingWriteRequestMapping mapPostToRelationship( - final String url, - final BodyParser bodyargs, - final RepositoryBackedRelationshipUrlResolver.RelationshipUrlResolution relationship) { - final Map args = bodyargs.getStringMap(); - String relationshipName = relationship.relationshipName(); - - EntityInstance connectThis = relationship.parentInstance(); - if (connectThis == null) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 404, - String.format("Could not find parent thing for relationship %s", url))); - } - - List possibleRelationships = - connectThis.getEntity().related().getRelationships(relationshipName); - RelationshipVectorDefinition relationshipToUse = possibleRelationships.get(0); - EntityDefinition thingTo = relationshipToUse.getTo(); - - EntityInstance relatedItem = null; - boolean amExpectingARelatedItem = false; - String matchingFieldNames = ""; - for (String fieldName : args.keySet()) { - final Field field = thingTo.getField(fieldName); - if (field == null) { - continue; - } - if (field.getType() == FieldType.AUTO_GUID - || field.getType() == FieldType.AUTO_INCREMENT) { - amExpectingARelatedItem = true; - if (!matchingFieldNames.contains(fieldName + " ")) { - matchingFieldNames = matchingFieldNames + fieldName + " "; - } - relatedItem = - store.entityQueries().findByField(thingTo, fieldName, args.get(fieldName)); - if (relatedItem != null) { - break; - } - } - } - if (amExpectingARelatedItem && relatedItem == null) { - matchingFieldNames = matchingFieldNames.trim().replace(" ", ", "); - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 404, - String.format( - "Could not find thing matching value for %s", - matchingFieldNames))); - } - - if (relatedItem == null) { - return bodyCommandMapper.mapCreateAndConnect( - bodyargs, connectThis, relationshipToUse.getName(), relationshipToUse.getTo()); - } - - relationshipToUse = - connectThis - .getEntity() - .getNamedRelationshipTo(relationshipName, relatedItem.getEntity()); - ApiMappingError relationshipError = - relationshipErrorIfInvalid( - connectThis, relatedItem, relationshipToUse, relationshipName); - if (relationshipError != null) { - return ThingWriteRequestMapping.error(relationshipError); - } - - return ThingWriteRequestMapping.command( - new ConnectExistingRelationshipCommand( - connectThis, relationshipToUse.getName(), relatedItem)); - } - - private ApiMappingError relationshipErrorIfInvalid( - final EntityInstance connectThis, - final EntityInstance relatedItem, - final RelationshipVectorDefinition relationshipToUse, - final String relationshipName) { - if (relationshipToUse == null) { - return ApiMappingError.withMessage( - 400, - String.format( - "Could not find a relationship named %s between %s and a %s", - relationshipName, - connectThis.getEntity().getName(), - relatedItem.getEntity().getName())); - } - - if (relationshipToUse.getTo() != relatedItem.getEntity()) { - return ApiMappingError.withMessage( - 400, - String.format( - "Could not connect %s (%s) to %s (%s) via relationship %s because it is a %s instead of a %s", - connectThis.getPrimaryKeyValue(), - connectThis.getEntity().getName(), - relatedItem.getPrimaryKeyValue(), - relatedItem.getEntity().getName(), - relationshipToUse.getName(), - relatedItem.getEntity().getName(), - relationshipToUse.getTo().getName())); - } - return null; - } - - private String fieldNames(final List fields) { - String names = ""; - for (Field field : fields) { - if (!names.isEmpty()) { - names = names + ", "; - } - names = names + field.getName(); - } - return names; - } -} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingWriteRequestMapping.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingWriteRequestMapping.java deleted file mode 100644 index cf099e54..00000000 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingWriteRequestMapping.java +++ /dev/null @@ -1,34 +0,0 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; - -import uk.co.compendiumdev.thingifier.application.command.ThingWriteCommand; - -public final class ThingWriteRequestMapping { - - private final ThingWriteCommand command; - private final ApiMappingError error; - - private ThingWriteRequestMapping(final ThingWriteCommand command, final ApiMappingError error) { - this.command = command; - this.error = error; - } - - public static ThingWriteRequestMapping command(final ThingWriteCommand command) { - return new ThingWriteRequestMapping(command, null); - } - - public static ThingWriteRequestMapping error(final ApiMappingError error) { - return new ThingWriteRequestMapping(null, error); - } - - public boolean isError() { - return error != null; - } - - public ThingWriteCommand getCommand() { - return command; - } - - public ApiMappingError getError() { - return error; - } -} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingifierSchemaCatalog.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingifierSchemaCatalog.java deleted file mode 100644 index 7b3014cc..00000000 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingifierSchemaCatalog.java +++ /dev/null @@ -1,23 +0,0 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; - -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; - -final class ThingifierSchemaCatalog implements SchemaCatalog { - - private final Thingifier thingifier; - - ThingifierSchemaCatalog(final Thingifier thingifier) { - this.thingifier = thingifier; - } - - @Override - public EntityDefinition definitionWithSingularOrPluralNamed(final String term) { - return thingifier.getERmodel().getSchema().getDefinitionWithSingularOrPluralNamed(term); - } - - @Override - public boolean hasRelationshipNamed(final String name) { - return thingifier.getERmodel().getSchema().hasRelationshipNamed(name); - } -} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/AmendThingHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/AmendThingHandler.java new file mode 100644 index 00000000..d8cebcf0 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/AmendThingHandler.java @@ -0,0 +1,143 @@ +package uk.co.compendiumdev.thingifier.application; + +import java.util.List; +import uk.co.compendiumdev.thingifier.application.command.AmendThingCommand; +import uk.co.compendiumdev.thingifier.application.command.RelationshipReference; +import uk.co.compendiumdev.thingifier.application.command.ReplaceThingCommand; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; +import uk.co.compendiumdev.thingifier.core.repository.ThingStore; + +final class AmendThingHandler { + + private final ThingStore store; + private final ThingDefinitionResolver definitions; + private final WriteValidationPolicy validation; + private final ThingDraftFactory drafts; + private final CreateThingHandler createHandler; + private final RelationshipConnectionService relationships; + + AmendThingHandler( + final ThingStore store, + final ThingDefinitionResolver definitions, + final WriteValidationPolicy validation, + final ThingDraftFactory drafts, + final CreateThingHandler createHandler, + final RelationshipConnectionService relationships) { + this.store = store; + this.definitions = definitions; + this.validation = validation; + this.drafts = drafts; + this.createHandler = createHandler; + this.relationships = relationships; + } + + ThingCommandResult handle(final AmendThingCommand command) { + EntityDefinition entity = definitions.entityNamed(command.getEntityName()); + ThingCommandResult typeValidation = + validation.validateDeclaredFieldTypesIgnoringProtected( + entity, command.getBodyFields()); + if (typeValidation != null) { + return typeValidation; + } + + EntityInstance instance = definitions.resolveInstance(entity, command.getIdentifier()); + if (instance == null) { + return ThingCommandResult.error( + ApplicationError.instanceNotFound( + command.getEntityName(), command.getIdentifier())); + } + + try { + List fieldValues = + validation.normalizedFieldValues( + entity, command.getFieldValues(), command.getBodyFields()); + EntityInstanceDraft draft = + new EntityInstanceDraftBuilder(instance).setFieldValuesFrom(fieldValues); + return amend( + instance, + draft, + command.shouldReplaceExistingFieldsAndRelationships(), + command.getRelationships()); + } catch (Exception e) { + return ThingCommandResult.error(ApplicationExceptionMessages.messageFrom(e)); + } + } + + ThingCommandResult handle(final ReplaceThingCommand command) { + EntityDefinition entity = definitions.entityNamed(command.getEntityName()); + ThingCommandResult typeValidation = + validation.validateDeclaredFieldTypesIgnoringProtected( + entity, command.getBodyFields()); + if (typeValidation != null) { + return typeValidation; + } + + List fieldValues = + validation.normalizedFieldValues( + entity, command.getFieldValues(), command.getBodyFields()); + EntityInstance instance = definitions.resolveInstance(entity, command.getIdentifier()); + if (instance != null) { + try { + EntityInstanceDraft draft = + new EntityInstanceDraftBuilder(instance).setFieldValuesFrom(fieldValues); + return amend(instance, draft, true, command.getRelationships()); + } catch (Exception e) { + return ThingCommandResult.error(ApplicationExceptionMessages.messageFrom(e)); + } + } + + ThingCommandResult creationAllowed = + validation.validateReplaceCreate(entity, command.getIdentifier(), fieldValues); + if (creationAllowed != null) { + return creationAllowed; + } + + try { + EntityInstanceDraft draft = + drafts.createDraft(entity, command.getIdentifier(), fieldValues); + ThingCommandResult created = + createHandler.create(draft, command.getRelationships(), true); + if (created.isError()) { + return created; + } + return ThingCommandResult.created(created.getInstance()); + } catch (Exception e) { + return ThingCommandResult.error(ApplicationExceptionMessages.messageFrom(e)); + } + } + + private ThingCommandResult amend( + final EntityInstance instance, + final EntityInstanceDraft draft, + final boolean replaceExistingFieldsAndRelationships, + final List relationshipReferences) { + RelationshipSnapshot originalRelationships = RelationshipSnapshot.capture(store, instance); + try { + EntityInstance updated; + if (replaceExistingFieldsAndRelationships) { + updated = store.entities().replace(instance, draft); + originalRelationships.disconnectFrom(store, updated); + } else { + updated = store.entities().patch(instance, draft); + } + + ThingCommandResult relationshipResult = + relationships.connectRelationshipReferences( + updated, relationshipReferences, true, false); + if (relationshipResult.isError()) { + return relationshipResult; + } + + if (replaceExistingFieldsAndRelationships) { + originalRelationships.deleteFormerDependentsMadeInvalidBy(store, updated); + } + + return ThingCommandResult.success(updated); + } catch (Exception e) { + return ThingCommandResult.error(ApplicationExceptionMessages.messageFrom(e)); + } + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ApplicationError.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ApplicationError.java new file mode 100644 index 00000000..40beefb9 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ApplicationError.java @@ -0,0 +1,172 @@ +package uk.co.compendiumdev.thingifier.application; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class ApplicationError { + + public enum Category { + VALIDATION, + NOT_FOUND, + CONFLICT, + UNSUPPORTED + } + + public enum Code { + VALIDATION_FAILED, + NOT_FOUND, + INSTANCE_NOT_FOUND, + PARENT_INSTANCE_NOT_FOUND, + RELATIONSHIP_SOURCE_NOT_FOUND, + RELATIONSHIP_TARGET_NOT_FOUND, + CONFLICT, + UNSUPPORTED_COMMAND, + REPLACE_CREATE_AUTO_FIELDS_NOT_ALLOWED, + REPLACE_CREATE_KEY_MISMATCH + } + + private final Category category; + private final Code code; + private final List messages; + private final Map details; + + private ApplicationError( + final Category category, + final Code code, + final Collection messages, + final Map details) { + this.category = category; + this.code = code; + this.messages = Collections.unmodifiableList(new ArrayList<>(messages)); + this.details = Collections.unmodifiableMap(new HashMap<>(details)); + } + + public static ApplicationError validation(final String message) { + return validation(List.of(message)); + } + + public static ApplicationError validation(final Collection messages) { + return new ApplicationError( + Category.VALIDATION, Code.VALIDATION_FAILED, messages, Map.of()); + } + + public static ApplicationError notFound(final String message) { + return new ApplicationError(Category.NOT_FOUND, Code.NOT_FOUND, List.of(message), Map.of()); + } + + public static ApplicationError instanceNotFound( + final String entityName, final String identifier) { + return new ApplicationError( + Category.NOT_FOUND, + Code.INSTANCE_NOT_FOUND, + List.of("Could not find instance"), + Map.of("entityName", entityName, "identifier", identifier)); + } + + public static ApplicationError parentInstanceNotFound( + final String entityName, final String identifier, final String relationshipName) { + return new ApplicationError( + Category.NOT_FOUND, + Code.PARENT_INSTANCE_NOT_FOUND, + List.of("Could not find relationship parent instance"), + Map.of( + "entityName", + entityName, + "identifier", + identifier, + "relationshipName", + relationshipName)); + } + + public static ApplicationError relationshipSourceNotFound( + final String entityName, final String identifier, final String relationshipName) { + return new ApplicationError( + Category.NOT_FOUND, + Code.RELATIONSHIP_SOURCE_NOT_FOUND, + List.of("Could not find relationship source instance"), + Map.of( + "entityName", + entityName, + "identifier", + identifier, + "relationshipName", + relationshipName)); + } + + public static ApplicationError relationshipTargetNotFound( + final String entityName, + final String identifier, + final String relationshipName, + final String childIdentifier) { + return new ApplicationError( + Category.NOT_FOUND, + Code.RELATIONSHIP_TARGET_NOT_FOUND, + List.of("Could not find related target instance"), + Map.of( + "entityName", + entityName, + "identifier", + identifier, + "relationshipName", + relationshipName, + "childIdentifier", + childIdentifier)); + } + + public static ApplicationError conflict(final String message) { + return new ApplicationError(Category.CONFLICT, Code.CONFLICT, List.of(message), Map.of()); + } + + public static ApplicationError unsupported(final String message) { + return new ApplicationError( + Category.UNSUPPORTED, Code.UNSUPPORTED_COMMAND, List.of(message), Map.of()); + } + + public static ApplicationError replaceCreateAutoFieldsNotAllowed( + final String entityName, final String fieldNames) { + return new ApplicationError( + Category.VALIDATION, + Code.REPLACE_CREATE_AUTO_FIELDS_NOT_ALLOWED, + List.of("Cannot create " + entityName + " because generated fields are present"), + Map.of("entityName", entityName, "fieldNames", fieldNames)); + } + + public static ApplicationError replaceCreateKeyMismatch( + final String entityName, final String routeIdentifier, final String bodyIdentifier) { + return new ApplicationError( + Category.VALIDATION, + Code.REPLACE_CREATE_KEY_MISMATCH, + List.of("Cannot create " + entityName + " because identity values do not match"), + Map.of( + "entityName", + entityName, + "routeIdentifier", + routeIdentifier, + "bodyIdentifier", + bodyIdentifier)); + } + + public Category category() { + return category; + } + + public Code code() { + return code; + } + + public List messages() { + return messages; + } + + public Map details() { + return details; + } + + public String detail(final String name) { + return details.get(name); + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ApplicationExceptionMessages.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ApplicationExceptionMessages.java new file mode 100644 index 00000000..d49ff06c --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ApplicationExceptionMessages.java @@ -0,0 +1,11 @@ +package uk.co.compendiumdev.thingifier.application; + +final class ApplicationExceptionMessages { + + private ApplicationExceptionMessages() {} + + static String messageFrom(final Exception exception) { + String message = exception.getMessage(); + return message == null ? "" : message; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/CreateThingHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/CreateThingHandler.java new file mode 100644 index 00000000..164e33ad --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/CreateThingHandler.java @@ -0,0 +1,87 @@ +package uk.co.compendiumdev.thingifier.application; + +import java.util.List; +import uk.co.compendiumdev.thingifier.application.command.CreateThingCommand; +import uk.co.compendiumdev.thingifier.application.command.RelationshipReference; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; +import uk.co.compendiumdev.thingifier.core.repository.ThingStore; + +final class CreateThingHandler { + + private final ThingStore store; + private final ThingDefinitionResolver definitions; + private final WriteValidationPolicy validation; + private final ThingDraftFactory drafts; + private final RelationshipConnectionService relationships; + + CreateThingHandler( + final ThingStore store, + final ThingDefinitionResolver definitions, + final WriteValidationPolicy validation, + final ThingDraftFactory drafts, + final RelationshipConnectionService relationships) { + this.store = store; + this.definitions = definitions; + this.validation = validation; + this.drafts = drafts; + this.relationships = relationships; + } + + ThingCommandResult handle(final CreateThingCommand command) { + EntityDefinition entity = definitions.entityNamed(command.getEntityName()); + if (entity == null) { + return ThingCommandResult.error( + ApplicationError.notFound( + String.format("Could not find entity %s", command.getEntityName()))); + } + + ThingCommandResult typeValidation = + validation.validateDeclaredFieldTypes(entity, command.getBodyFields()); + if (typeValidation != null) { + return typeValidation; + } + + List fieldValues = + validation.normalizedFieldValues( + entity, command.getFieldValues(), command.getBodyFields()); + ThingCommandResult validationResult = + validation.validateCreate( + entity, + fieldValues, + command.hasRequestedPrimaryKey(), + command.getRequestedPrimaryKey()); + if (validationResult != null) { + return validationResult; + } + + try { + EntityInstanceDraft draft = + drafts.createDraft(entity, command.getRequestedPrimaryKey(), fieldValues); + return create( + draft, command.getRelationships(), command.shouldValidateFinalRelationships()); + } catch (Exception e) { + return ThingCommandResult.error(ApplicationExceptionMessages.messageFrom(e)); + } + } + + ThingCommandResult create( + final EntityInstanceDraft draft, + final List relationshipReferences, + final boolean validateFinalRelationships) { + try { + EntityInstance created = store.entities().create(draft); + ThingCommandResult relationshipResult = + relationships.connectRelationshipReferences( + created, relationshipReferences, validateFinalRelationships, true); + if (relationshipResult.isError()) { + return relationshipResult; + } + return ThingCommandResult.success(created); + } catch (Exception e) { + return ThingCommandResult.error(ApplicationExceptionMessages.messageFrom(e)); + } + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/DeleteThingHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/DeleteThingHandler.java new file mode 100644 index 00000000..baf977ea --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/DeleteThingHandler.java @@ -0,0 +1,34 @@ +package uk.co.compendiumdev.thingifier.application; + +import uk.co.compendiumdev.thingifier.application.command.DeleteThingCommand; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; +import uk.co.compendiumdev.thingifier.core.repository.ThingStore; + +final class DeleteThingHandler { + + private final ThingStore store; + private final ThingDefinitionResolver definitions; + + DeleteThingHandler(final ThingStore store, final ThingDefinitionResolver definitions) { + this.store = store; + this.definitions = definitions; + } + + ThingCommandResult handle(final DeleteThingCommand command) { + EntityDefinition entity = definitions.entityNamed(command.getEntityName()); + EntityInstance instance = definitions.resolveInstance(entity, command.getIdentifier()); + if (instance == null) { + return ThingCommandResult.error( + ApplicationError.instanceNotFound( + command.getEntityName(), command.getIdentifier())); + } + + try { + store.entities().delete(instance); + return ThingCommandResult.success(); + } catch (Exception e) { + return ThingCommandResult.error(ApplicationExceptionMessages.messageFrom(e)); + } + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/EntityInstanceDraftBuilder.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/EntityInstanceDraftBuilder.java new file mode 100644 index 00000000..cee02260 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/EntityInstanceDraftBuilder.java @@ -0,0 +1,106 @@ +package uk.co.compendiumdev.thingifier.application; + +import java.util.ArrayList; +import java.util.List; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; + +public final class EntityInstanceDraftBuilder { + + private final EntityDefinition entity; + private final EntityInstance instance; + + public EntityInstanceDraftBuilder(final EntityDefinition entity) { + this.entity = entity; + this.instance = null; + } + + public EntityInstanceDraftBuilder(final EntityInstance instance) { + this.entity = instance.getEntity(); + this.instance = instance; + } + + public EntityInstanceDraft setFieldValuesFrom(final List fieldValues) { + if (instance != null) { + final List errors = findAnyGuidOrIdDifferences(instance, fieldValues); + if (!errors.isEmpty()) { + throw new RuntimeException(errors.get(0)); + } + } + + return setFieldValuesFromArgsIgnoring( + fieldValues, + entity.getFieldNamesOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID)); + } + + public EntityInstanceDraft setFieldValuesFromArgsIgnoring( + final List fieldValues, final List ignoreFields) { + EntityInstanceDraft draft = EntityInstanceDraft.forEntity(entity); + for (NamedValue entry : fieldValues) { + if (!ignoreFields.contains(entry.getName())) { + draft.withField(entry.getName(), entry.asString()); + } + } + return draft; + } + + public EntityInstanceDraft overrideFieldValuesFromArgsIgnoring( + final List fieldValues, final List ignoreFields) { + EntityInstanceDraft draft = EntityInstanceDraft.forEntity(entity); + for (NamedValue entry : fieldValues) { + if (!ignoreFields.contains(entry.getName())) { + if (isProtectedField(entry.getName())) { + draft.withProtectedField(entry.getName(), entry.asString()); + } else { + draft.withField(entry.getName(), entry.asString()); + } + } + } + return draft; + } + + private boolean isProtectedField(final String fieldName) { + Field field = entity.getField(fieldName); + return field != null + && (field.getType() == FieldType.AUTO_INCREMENT + || field.getType() == FieldType.AUTO_GUID); + } + + private List findAnyGuidOrIdDifferences( + final EntityInstance instance, final List fieldValues) { + List errorMessages = new ArrayList<>(); + + for (NamedValue entry : fieldValues) { + Field field = instance.getEntity().getField(entry.name); + if (field == null + || (field.getType() != FieldType.AUTO_INCREMENT + && field.getType() != FieldType.AUTO_GUID)) { + continue; + } + + String existingValue = instance.getFieldValue(entry.name).asString(); + String entryValue = entry.value; + if (field.getType() == FieldType.AUTO_INCREMENT) { + try { + entryValue = String.valueOf((int) Float.parseFloat(entryValue)); + } catch (Exception e) { + // keep the original value for comparison + } + } + if (existingValue != null + && !existingValue.trim().isEmpty() + && !existingValue.equalsIgnoreCase(entryValue)) { + errorMessages.add( + String.format( + "Can not amend %s from %s to %s", + entry.name, existingValue, entryValue)); + } + } + + return errorMessages; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipCommandHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipCommandHandler.java new file mode 100644 index 00000000..defa454f --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipCommandHandler.java @@ -0,0 +1,254 @@ +package uk.co.compendiumdev.thingifier.application; + +import java.util.List; +import uk.co.compendiumdev.thingifier.application.command.ConnectExistingRelationshipCommand; +import uk.co.compendiumdev.thingifier.application.command.CreateAndConnectRelationshipCommand; +import uk.co.compendiumdev.thingifier.application.command.DisconnectRelationshipCommand; +import uk.co.compendiumdev.thingifier.application.command.RelateThingCommand; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; +import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipVectorDefinition; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; +import uk.co.compendiumdev.thingifier.core.repository.ThingStore; + +final class RelationshipCommandHandler { + + private final ThingStore store; + private final ThingDefinitionResolver definitions; + private final WriteValidationPolicy validation; + private final ThingDraftFactory drafts; + private final CreateThingHandler createHandler; + private final RelationshipConnectionService relationships; + private final RelationshipTargetResolver targets; + + RelationshipCommandHandler( + final ThingStore store, + final ThingDefinitionResolver definitions, + final WriteValidationPolicy validation, + final ThingDraftFactory drafts, + final CreateThingHandler createHandler, + final RelationshipConnectionService relationships, + final RelationshipTargetResolver targets) { + this.store = store; + this.definitions = definitions; + this.validation = validation; + this.drafts = drafts; + this.createHandler = createHandler; + this.relationships = relationships; + this.targets = targets; + } + + ThingCommandResult handle(final ConnectExistingRelationshipCommand command) { + EntityDefinition parentEntity = definitions.entityNamed(command.getParentEntityName()); + EntityInstance parent = + definitions.resolveInstance(parentEntity, command.getParentIdentifier()); + if (parent == null) { + return parentNotFound(command); + } + + RelationshipTargetResolver.RelatedItemResolution related = + targets.resolveRelatedItemFromReferenceFields( + parent, command.getRelationshipName(), command.getChildReferenceFields()); + if (related.error() != null) { + return ThingCommandResult.error(related.error()); + } + + RelationshipVectorDefinition relationshipToUse = + parent.getEntity() + .getNamedRelationshipTo( + command.getRelationshipName(), related.instance().getEntity()); + ThingCommandResult relationshipError = + relationships.relationshipErrorIfInvalid( + parent, + related.instance(), + relationshipToUse, + command.getRelationshipName()); + if (relationshipError != null) { + return relationshipError; + } + + return relationships.connectRelationship( + parent, relationshipToUse.getName(), related.instance(), false); + } + + ThingCommandResult handle(final CreateAndConnectRelationshipCommand command) { + EntityDefinition parentEntity = definitions.entityNamed(command.getParentEntityName()); + EntityInstance parent = + definitions.resolveInstance(parentEntity, command.getParentIdentifier()); + if (parent == null) { + return parentNotFound(command); + } + + EntityDefinition childEntity = definitions.entityNamed(command.getChildEntityName()); + ThingCommandResult typeValidation = + validation.validateDeclaredFieldTypes(childEntity, command.getChildBodyFields()); + if (typeValidation != null) { + return typeValidation; + } + + List childFieldValues = + validation.normalizedFieldValues( + childEntity, command.getChildFieldValues(), command.getChildBodyFields()); + ThingCommandResult validationResult = + validation.validateCreate(childEntity, childFieldValues, false, ""); + if (validationResult != null) { + return validationResult; + } + + try { + EntityInstanceDraft childDraft = drafts.createDraft(childEntity, "", childFieldValues); + ThingCommandResult createResult = + createHandler.create(childDraft, command.getChildRelationships(), false); + if (createResult.isError()) { + return createResult; + } + + ThingCommandResult connectResult = + relationships.connectRelationship( + parent, + command.getRelationshipName(), + createResult.getInstance(), + true); + if (connectResult.isError()) { + return connectResult.withRolledBackCreatedInstance(); + } + + return ThingCommandResult.success(createResult.getInstance()); + } catch (Exception e) { + return ThingCommandResult.error(ApplicationExceptionMessages.messageFrom(e)); + } + } + + ThingCommandResult handle(final RelateThingCommand command) { + EntityDefinition parentEntity = definitions.entityNamed(command.getParentEntityName()); + if (parentEntity == null) { + return parentNotFound(command); + } + + RelationshipVectorDefinition vector = + targets.firstRelationshipVector(parentEntity, command.getRelationshipName()); + if (vector == null) { + return ThingCommandResult.error( + String.format( + "Could not find a relationship named %s for %s", + command.getRelationshipName(), parentEntity.getName())); + } + + List bodyFieldValues = + validation.normalizedFieldValues( + vector.getTo(), command.getBodyFieldValues(), command.getBodyFields()); + boolean referencesExistingRelatedItem = + targets.bodyReferencesExistingRelatedItem(vector.getTo(), bodyFieldValues); + + ThingCommandResult typeValidation = + referencesExistingRelatedItem + ? validation.validateDeclaredFieldTypesIgnoringProtected( + vector.getTo(), command.getBodyFields()) + : validation.validateDeclaredFieldTypes( + vector.getTo(), command.getBodyFields()); + if (typeValidation != null) { + return typeValidation; + } + + EntityInstance parent = + definitions.resolveInstance(parentEntity, command.getParentIdentifier()); + if (parent == null) { + return parentNotFound(command); + } + + if (referencesExistingRelatedItem) { + ConnectExistingRelationshipCommand connect = + new ConnectExistingRelationshipCommand( + command.getParentEntityName(), + command.getParentIdentifier(), + command.getRelationshipName(), + bodyFieldValues); + return handle(connect); + } + + CreateAndConnectRelationshipCommand create = + new CreateAndConnectRelationshipCommand( + command.getParentEntityName(), + command.getParentIdentifier(), + command.getRelationshipName(), + vector.getTo().getName(), + bodyFieldValues, + command.getBodyFields(), + command.getBodyRelationships()); + ThingCommandResult result = handle(create); + if (result.isSuccessful()) { + return ThingCommandResult.created(result.getInstance()); + } + return result; + } + + ThingCommandResult handle(final DisconnectRelationshipCommand command) { + EntityDefinition parentEntity = definitions.entityNamed(command.getParentEntityName()); + EntityInstance parent = + definitions.resolveInstance(parentEntity, command.getParentIdentifier()); + if (parent == null) { + return relationshipSourceNotFound(command); + } + + EntityInstance child = + targets.relatedInstanceMatchingIdentifier( + parent, command.getRelationshipName(), command.getChildIdentifier()); + if (child == null) { + return relationshipTargetNotFound(command); + } + + try { + store.relationships().removeBetween(parent, child, command.getRelationshipName()); + return ThingCommandResult.success(); + } catch (Exception e) { + return ThingCommandResult.error(ApplicationExceptionMessages.messageFrom(e)); + } + } + + private ThingCommandResult parentNotFound(final ConnectExistingRelationshipCommand command) { + return parentNotFound( + command.getParentEntityName(), + command.getParentIdentifier(), + command.getRelationshipName()); + } + + private ThingCommandResult parentNotFound(final CreateAndConnectRelationshipCommand command) { + return parentNotFound( + command.getParentEntityName(), + command.getParentIdentifier(), + command.getRelationshipName()); + } + + private ThingCommandResult parentNotFound(final RelateThingCommand command) { + return parentNotFound( + command.getParentEntityName(), + command.getParentIdentifier(), + command.getRelationshipName()); + } + + private ThingCommandResult parentNotFound( + final String entityName, final String identifier, final String relationshipName) { + return ThingCommandResult.error( + ApplicationError.parentInstanceNotFound(entityName, identifier, relationshipName)); + } + + private ThingCommandResult relationshipSourceNotFound( + final DisconnectRelationshipCommand command) { + return ThingCommandResult.error( + ApplicationError.relationshipSourceNotFound( + command.getParentEntityName(), + command.getParentIdentifier(), + command.getRelationshipName())); + } + + private ThingCommandResult relationshipTargetNotFound( + final DisconnectRelationshipCommand command) { + return ThingCommandResult.error( + ApplicationError.relationshipTargetNotFound( + command.getParentEntityName(), + command.getParentIdentifier(), + command.getRelationshipName(), + command.getChildIdentifier())); + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipConnectionService.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipConnectionService.java new file mode 100644 index 00000000..998795d3 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipConnectionService.java @@ -0,0 +1,158 @@ +package uk.co.compendiumdev.thingifier.application; + +import java.util.ArrayList; +import java.util.List; +import uk.co.compendiumdev.thingifier.application.command.RelationshipReference; +import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipVectorDefinition; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; +import uk.co.compendiumdev.thingifier.core.reporting.ValidationReport; +import uk.co.compendiumdev.thingifier.core.repository.ThingStore; + +final class RelationshipConnectionService { + + private final ThingStore store; + private final RelationshipReferenceResolver relationshipResolver; + + RelationshipConnectionService( + final ThingStore store, final RelationshipReferenceResolver relationshipResolver) { + this.store = store; + this.relationshipResolver = relationshipResolver; + } + + ThingCommandResult connectRelationship( + final EntityInstance parent, + final String relationshipName, + final EntityInstance child, + final boolean deleteChildOnRollback) { + boolean alreadyConnected = + isRelated(parent, new RelationshipConnection(relationshipName, child)); + try { + store.relationships().connect(parent, relationshipName, child); + + ValidationReport validNow = store.relationships().validate(child); + if (!validNow.isValid()) { + if (!alreadyConnected) { + store.relationships().disconnectBetween(parent, child, relationshipName); + } + if (deleteChildOnRollback) { + store.entities().delete(child); + } + return ThingCommandResult.error(validNow.getErrorMessages()); + } + + return ThingCommandResult.success(child); + } catch (Exception e) { + if (!alreadyConnected) { + store.relationships().disconnectBetween(parent, child, relationshipName); + } + if (deleteChildOnRollback) { + store.entities().delete(child); + } + return ThingCommandResult.error(ApplicationExceptionMessages.messageFrom(e)); + } + } + + ThingCommandResult connectRelationshipReferences( + final EntityInstance instance, + final List references, + final boolean validateFinalRelationships, + final boolean prefixRelationshipErrors) { + RelationshipReferenceResolver.Resolution resolution = + relationshipResolver.resolve(instance, references); + if (resolution.hasErrors()) { + if (prefixRelationshipErrors) { + return ThingCommandResult.error( + "Invalid relationships: " + String.join(", ", resolution.errors())); + } + return ThingCommandResult.error(resolution.errors()); + } + + return connectRelationships( + instance, resolution.relationships(), validateFinalRelationships); + } + + ThingCommandResult relationshipErrorIfInvalid( + final EntityInstance parent, + final EntityInstance child, + final RelationshipVectorDefinition relationshipToUse, + final String relationshipName) { + if (relationshipToUse == null) { + return ThingCommandResult.error( + String.format( + "Could not find a relationship named %s between %s and a %s", + relationshipName, + parent.getEntity().getName(), + child.getEntity().getName())); + } + + if (relationshipToUse.getTo() != child.getEntity()) { + return ThingCommandResult.error( + String.format( + "Could not connect %s (%s) to %s (%s) via relationship %s because it is a %s instead of a %s", + parent.getPrimaryKeyValue(), + parent.getEntity().getName(), + child.getPrimaryKeyValue(), + child.getEntity().getName(), + relationshipToUse.getName(), + child.getEntity().getName(), + relationshipToUse.getTo().getName())); + } + return null; + } + + private ThingCommandResult connectRelationships( + final EntityInstance instance, + final List relationships, + final boolean validateFinalRelationships) { + List connectedByCommand = new ArrayList<>(); + try { + for (RelationshipConnection relationship : relationships) { + boolean alreadyConnected = isRelated(instance, relationship); + store.relationships() + .connect( + instance, + relationship.relationshipName(), + relationship.relatedInstance()); + if (!alreadyConnected) { + connectedByCommand.add(relationship); + } + } + + if (validateFinalRelationships) { + ValidationReport finalRelationships = store.relationships().validate(instance); + if (!finalRelationships.isValid()) { + disconnectConnections(instance, connectedByCommand); + return ThingCommandResult.error(finalRelationships.getErrorMessages()); + } + } + + return ThingCommandResult.success(instance); + } catch (Exception e) { + disconnectConnections(instance, connectedByCommand); + return ThingCommandResult.error( + "Error creating relationships " + ApplicationExceptionMessages.messageFrom(e)); + } + } + + private void disconnectConnections( + final EntityInstance instance, final List relationships) { + for (RelationshipConnection relationship : relationships) { + store.relationships() + .disconnectBetween( + instance, + relationship.relatedInstance(), + relationship.relationshipName()); + } + } + + private boolean isRelated( + final EntityInstance instance, final RelationshipConnection relationship) { + for (EntityInstance related : + store.relationships().listRelated(instance, relationship.relationshipName())) { + if (related.getInternalId().equals(relationship.relatedInstance().getInternalId())) { + return true; + } + } + return false; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipReferenceResolver.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipReferenceResolver.java new file mode 100644 index 00000000..eed53baf --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipReferenceResolver.java @@ -0,0 +1,125 @@ +package uk.co.compendiumdev.thingifier.application; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import uk.co.compendiumdev.thingifier.application.command.RelationshipReference; +import uk.co.compendiumdev.thingifier.application.schema.SchemaDefinitionResolver; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipVectorDefinition; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; +import uk.co.compendiumdev.thingifier.core.repository.ThingStore; + +final class RelationshipReferenceResolver { + + private final ThingStore store; + private final SchemaDefinitionResolver schema; + + RelationshipReferenceResolver(final ThingStore store, final SchemaDefinitionResolver schema) { + this.store = store; + this.schema = schema; + } + + Resolution resolve( + final EntityInstance instance, final List references) { + List relationships = new ArrayList<>(); + List errors = new ArrayList<>(); + + for (RelationshipReference reference : references) { + EntityInstance related = resolveRelationshipReference(instance, reference); + if (related == null) { + errors.add(missingRelationshipReferenceMessage(reference)); + } else { + relationships.add( + new RelationshipConnection(reference.relationshipName(), related)); + } + } + + return new Resolution(relationships, errors); + } + + private EntityInstance resolveRelationshipReference( + final EntityInstance instance, final RelationshipReference reference) { + if (reference.hasExplicitTargetEntity()) { + return resolveExplicitRelationshipReference(reference); + } + + for (RelationshipVectorDefinition vector : + instance.getEntity().related().getRelationships(reference.relationshipName())) { + EntityDefinition relatedEntity = vector.getTo(); + EntityInstance related = + store.entityQueries() + .findByField( + relatedEntity, + reference.referenceFieldName(), + reference.referenceValue()); + if (related == null) { + related = + store.entityQueries() + .findByQueryIdentifier(relatedEntity, reference.referenceValue()); + } + if (related != null) { + return related; + } + } + + return null; + } + + private EntityInstance resolveExplicitRelationshipReference( + final RelationshipReference reference) { + EntityDefinition target = schema.entityNamed(reference.targetEntityName()); + EntityInstance related = + store.entityQueries().findByQueryIdentifier(target, reference.referenceValue()); + if (related == null) { + related = + store.entityQueries() + .findByField( + target, + reference.referenceFieldName(), + reference.referenceValue()); + } + return related; + } + + private String missingRelationshipReferenceMessage(final RelationshipReference reference) { + if (reference.hasExplicitTargetEntity()) { + return String.format( + "cannot find %s of %s to relate to with %s %s", + reference.referenceFieldName(), + reference.targetTerm(), + reference.referenceFieldName(), + reference.referenceValue()); + } + + return String.format( + "cannot find %s to relate to with %s %s", + reference.relationshipName(), + reference.referenceFieldName(), + reference.referenceValue()); + } + + static final class Resolution { + + private final List relationships; + private final List errors; + + private Resolution( + final List relationships, final List errors) { + this.relationships = Collections.unmodifiableList(new ArrayList<>(relationships)); + this.errors = Collections.unmodifiableList(new ArrayList<>(errors)); + } + + boolean hasErrors() { + return !errors.isEmpty(); + } + + List relationships() { + return relationships; + } + + List errors() { + return errors; + } + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipSnapshot.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipSnapshot.java new file mode 100644 index 00000000..7f32797f --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipSnapshot.java @@ -0,0 +1,81 @@ +package uk.co.compendiumdev.thingifier.application; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipVectorDefinition; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; +import uk.co.compendiumdev.thingifier.core.repository.ThingStore; + +final class RelationshipSnapshot { + + private final List links; + + private RelationshipSnapshot(final List links) { + this.links = links; + } + + static RelationshipSnapshot capture(final ThingStore store, final EntityInstance instance) { + List links = new ArrayList<>(); + Set seenLinks = new HashSet<>(); + for (RelationshipVectorDefinition vector : + instance.getEntity().related().getRelationships()) { + for (EntityInstance related : + store.relationships().listRelated(instance, vector.getName())) { + String key = vector.getName() + "|" + related.getInternalId(); + if (seenLinks.add(key)) { + links.add( + new RelationshipLink( + vector.getName(), + related, + store.relationships().validate(related).isValid())); + } + } + } + return new RelationshipSnapshot(links); + } + + void disconnectFrom(final ThingStore store, final EntityInstance instance) { + for (RelationshipLink link : links) { + store.relationships().disconnectBetween(instance, link.related, link.relationshipName); + } + } + + void deleteFormerDependentsMadeInvalidBy( + final ThingStore store, final EntityInstance instance) { + for (RelationshipLink link : links) { + if (link.relatedWasValid + && !link.isStillRelatedTo(store, instance) + && !store.relationships().validate(link.related).isValid()) { + store.entities().delete(link.related); + } + } + } + + private static final class RelationshipLink { + + private final String relationshipName; + private final EntityInstance related; + private final boolean relatedWasValid; + + private RelationshipLink( + final String relationshipName, + final EntityInstance related, + final boolean relatedWasValid) { + this.relationshipName = relationshipName; + this.related = related; + this.relatedWasValid = relatedWasValid; + } + + private boolean isStillRelatedTo(final ThingStore store, final EntityInstance instance) { + for (EntityInstance current : + store.relationships().listRelated(instance, relationshipName)) { + if (current.getInternalId().equals(related.getInternalId())) { + return true; + } + } + return false; + } + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipTargetResolver.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipTargetResolver.java new file mode 100644 index 00000000..8352c843 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipTargetResolver.java @@ -0,0 +1,147 @@ +package uk.co.compendiumdev.thingifier.application; + +import java.util.List; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; +import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipVectorDefinition; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; +import uk.co.compendiumdev.thingifier.core.repository.ThingStore; + +final class RelationshipTargetResolver { + + private final ThingStore store; + + RelationshipTargetResolver(final ThingStore store) { + this.store = store; + } + + RelatedItemResolution resolveRelatedItemFromReferenceFields( + final EntityInstance parent, + final String relationshipName, + final List childReferenceFields) { + List possibleRelationships = + parent.getEntity().related().getRelationships(relationshipName); + RelationshipVectorDefinition relationshipToUse = possibleRelationships.get(0); + EntityDefinition targetEntity = relationshipToUse.getTo(); + + EntityInstance relatedItem = null; + boolean expectingRelatedItem = false; + String matchingFieldNames = ""; + for (NamedValue fieldValue : childReferenceFields) { + final Field field = targetEntity.getField(fieldValue.getName()); + if (field == null) { + continue; + } + if (field.getType() == FieldType.AUTO_GUID + || field.getType() == FieldType.AUTO_INCREMENT) { + expectingRelatedItem = true; + if (!matchingFieldNames.contains(fieldValue.getName() + " ")) { + matchingFieldNames = matchingFieldNames + fieldValue.getName() + " "; + } + relatedItem = + store.entityQueries() + .findByField( + targetEntity, fieldValue.getName(), fieldValue.asString()); + if (relatedItem != null) { + break; + } + } + } + if (expectingRelatedItem && relatedItem == null) { + matchingFieldNames = matchingFieldNames.trim().replace(" ", ", "); + return RelatedItemResolution.error( + ApplicationError.notFound( + String.format( + "Could not find thing matching value for %s", + matchingFieldNames))); + } + + if (relatedItem == null) { + return RelatedItemResolution.error( + ApplicationError.validation( + String.format( + "No related item reference supplied for %s", + relationshipName))); + } + + return RelatedItemResolution.success(relatedItem); + } + + RelationshipVectorDefinition firstRelationshipVector( + final EntityDefinition entity, final String relationshipName) { + List vectors = + entity.related().getRelationships(relationshipName); + if (vectors.isEmpty()) { + return null; + } + return vectors.get(0); + } + + boolean bodyReferencesExistingRelatedItem( + final EntityDefinition targetEntity, final List bodyFields) { + for (NamedValue fieldValue : bodyFields) { + Field field = targetEntity.getField(fieldValue.getName()); + if (field == null) { + continue; + } + if (field.getType() == FieldType.AUTO_GUID + || field.getType() == FieldType.AUTO_INCREMENT) { + return true; + } + } + return false; + } + + EntityInstance relatedInstanceMatchingIdentifier( + final EntityInstance parent, final String relationshipName, final String identifier) { + for (EntityInstance related : store.relationships().listRelated(parent, relationshipName)) { + if (matchesQueryIdentifier(related, identifier)) { + return related; + } + } + return null; + } + + private boolean matchesQueryIdentifier(final EntityInstance instance, final String identifier) { + for (Field autoIncrementField : + instance.getEntity().getFieldsOfType(FieldType.AUTO_INCREMENT)) { + String idValue = instance.getFieldValue(autoIncrementField.getName()).asString(); + if (idValue.contentEquals(identifier)) { + return true; + } + break; + } + + String primaryKeyValue = instance.getPrimaryKeyValue(); + return primaryKeyValue != null && primaryKeyValue.contentEquals(identifier); + } + + static final class RelatedItemResolution { + + private final EntityInstance instance; + private final ApplicationError error; + + private RelatedItemResolution(final EntityInstance instance, final ApplicationError error) { + this.instance = instance; + this.error = error; + } + + static RelatedItemResolution success(final EntityInstance instance) { + return new RelatedItemResolution(instance, null); + } + + static RelatedItemResolution error(final ApplicationError error) { + return new RelatedItemResolution(null, error); + } + + EntityInstance instance() { + return instance; + } + + ApplicationError error() { + return error; + } + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingCommandResult.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingCommandResult.java index 4b21193c..943007e2 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingCommandResult.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingCommandResult.java @@ -12,39 +12,54 @@ public final class ThingCommandResult { private final EntityInstance instance; private final List errorMessages; private final boolean rolledBackCreatedInstance; + private final ApplicationError error; + private final boolean createdInstance; private ThingCommandResult( final boolean successful, final EntityInstance instance, final List errorMessages, - final boolean rolledBackCreatedInstance) { + final boolean rolledBackCreatedInstance, + final ApplicationError error, + final boolean createdInstance) { this.successful = successful; this.instance = instance; this.errorMessages = Collections.unmodifiableList(new ArrayList<>(errorMessages)); this.rolledBackCreatedInstance = rolledBackCreatedInstance; + this.error = error; + this.createdInstance = createdInstance; } public static ThingCommandResult success() { - return new ThingCommandResult(true, null, List.of(), false); + return new ThingCommandResult(true, null, List.of(), false, null, false); } public static ThingCommandResult success(final EntityInstance instance) { - return new ThingCommandResult(true, instance, List.of(), false); + return new ThingCommandResult(true, instance, List.of(), false, null, false); + } + + public static ThingCommandResult created(final EntityInstance instance) { + return new ThingCommandResult(true, instance, List.of(), false, null, true); } public static ThingCommandResult error(final String message) { if (message == null || message.isEmpty()) { - return new ThingCommandResult(false, null, List.of(""), false); + return error(ApplicationError.validation("")); } - return new ThingCommandResult(false, null, List.of(message), false); + return error(ApplicationError.validation(message)); } public static ThingCommandResult error(final Collection messages) { - return new ThingCommandResult(false, null, new ArrayList<>(messages), false); + return error(ApplicationError.validation(messages)); + } + + public static ThingCommandResult error(final ApplicationError error) { + return new ThingCommandResult(false, null, error.messages(), false, error, false); } public ThingCommandResult withRolledBackCreatedInstance() { - return new ThingCommandResult(successful, instance, errorMessages, true); + return new ThingCommandResult( + successful, instance, errorMessages, true, error, createdInstance); } public boolean isSuccessful() { @@ -70,4 +85,12 @@ public String getCombinedErrorMessage() { public boolean rolledBackCreatedInstance() { return rolledBackCreatedInstance; } + + public ApplicationError getError() { + return error; + } + + public boolean createdInstance() { + return createdInstance; + } } diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingCommandService.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingCommandService.java index 130c9322..c60afdc8 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingCommandService.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingCommandService.java @@ -1,499 +1,105 @@ package uk.co.compendiumdev.thingifier.application; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; import uk.co.compendiumdev.thingifier.application.command.AmendThingCommand; import uk.co.compendiumdev.thingifier.application.command.ConnectExistingRelationshipCommand; import uk.co.compendiumdev.thingifier.application.command.CreateAndConnectRelationshipCommand; import uk.co.compendiumdev.thingifier.application.command.CreateThingCommand; import uk.co.compendiumdev.thingifier.application.command.DeleteThingCommand; import uk.co.compendiumdev.thingifier.application.command.DisconnectRelationshipCommand; -import uk.co.compendiumdev.thingifier.application.command.RelationshipReference; +import uk.co.compendiumdev.thingifier.application.command.RelateThingCommand; +import uk.co.compendiumdev.thingifier.application.command.ReplaceThingCommand; import uk.co.compendiumdev.thingifier.application.command.ThingWriteCommand; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.FieldValue; -import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipVectorDefinition; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; -import uk.co.compendiumdev.thingifier.core.reporting.ValidationReport; +import uk.co.compendiumdev.thingifier.application.schema.SchemaDefinitionResolver; import uk.co.compendiumdev.thingifier.core.repository.ThingStore; public final class ThingCommandService { - private final ThingStore store; - - public ThingCommandService(final ThingStore store) { - this.store = store; + private final WriteTransactionRunner transactionRunner; + private final CreateThingHandler createHandler; + private final AmendThingHandler amendHandler; + private final DeleteThingHandler deleteHandler; + private final RelationshipCommandHandler relationshipHandler; + + public ThingCommandService(final ThingStore store, final SchemaDefinitionResolver schema) { + this(store, schema, false); + } + + public ThingCommandService( + final ThingStore store, + final SchemaDefinitionResolver schema, + final boolean enforceDeclaredTypes) { + ThingDefinitionResolver definitions = new ThingDefinitionResolver(store, schema); + WriteValidationPolicy validation = new WriteValidationPolicy(store, enforceDeclaredTypes); + RelationshipReferenceResolver relationshipResolver = + new RelationshipReferenceResolver(store, schema); + RelationshipConnectionService relationshipConnections = + new RelationshipConnectionService(store, relationshipResolver); + ThingDraftFactory drafts = new ThingDraftFactory(store); + + this.transactionRunner = new WriteTransactionRunner(store); + this.createHandler = + new CreateThingHandler( + store, definitions, validation, drafts, relationshipConnections); + this.amendHandler = + new AmendThingHandler( + store, + definitions, + validation, + drafts, + createHandler, + relationshipConnections); + this.deleteHandler = new DeleteThingHandler(store, definitions); + this.relationshipHandler = + new RelationshipCommandHandler( + store, + definitions, + validation, + drafts, + createHandler, + relationshipConnections, + new RelationshipTargetResolver(store)); } public ThingCommandResult execute(final ThingWriteCommand command) { + return transactionRunner.run(() -> executeInsideTransaction(command)); + } + + private ThingCommandResult executeInsideTransaction(final ThingWriteCommand command) { if (command instanceof CreateThingCommand) { - CreateThingCommand create = (CreateThingCommand) command; - return create( - create.getDraft(), - create.getRelationships(), - create.shouldValidateFinalRelationships()); + return createHandler.handle((CreateThingCommand) command); } if (command instanceof AmendThingCommand) { - AmendThingCommand amend = (AmendThingCommand) command; - return amend( - amend.getInstance(), - amend.getDraft(), - amend.shouldReplaceExistingFieldsAndRelationships(), - amend.getRelationships()); + return amendHandler.handle((AmendThingCommand) command); } if (command instanceof DeleteThingCommand) { - return delete(((DeleteThingCommand) command).getInstance()); - } - - if (command instanceof ConnectExistingRelationshipCommand) { - ConnectExistingRelationshipCommand connect = - (ConnectExistingRelationshipCommand) command; - return connectRelationship( - connect.getParent(), connect.getRelationshipName(), connect.getChild(), false); - } - - if (command instanceof CreateAndConnectRelationshipCommand) { - return createAndConnect((CreateAndConnectRelationshipCommand) command); - } - - if (command instanceof DisconnectRelationshipCommand) { - DisconnectRelationshipCommand disconnect = (DisconnectRelationshipCommand) command; - return disconnectRelationship( - disconnect.getParent(), - disconnect.getChild(), - disconnect.getRelationshipName()); - } - - return ThingCommandResult.error( - String.format("Unsupported command %s", command.getClass().getSimpleName())); - } - - public ThingCommandResult create( - final EntityInstanceDraft draft, final List relationships) { - return create(draft, relationships, true); - } - - public ThingCommandResult create( - final EntityInstanceDraft draft, - final List relationships, - final boolean validateFinalRelationships) { - EntityInstance created = null; - try { - created = store.entities().create(draft); - ThingCommandResult relationshipResult = - connectRelationshipReferences( - created, relationships, validateFinalRelationships, true); - if (relationshipResult.isError()) { - rollbackDelete(created); - return relationshipResult; - } - return ThingCommandResult.success(created); - } catch (Exception e) { - rollbackDelete(created); - return ThingCommandResult.error(messageFrom(e)); - } - } - - public ThingCommandResult amend( - final EntityInstance instance, - final EntityInstanceDraft draft, - final boolean replaceExistingFieldsAndRelationships, - final List relationships) { - EntityInstance updated = null; - RelationshipSnapshot originalRelationships = RelationshipSnapshot.capture(store, instance); - try { - if (replaceExistingFieldsAndRelationships) { - updated = store.entities().replace(instance, draft); - originalRelationships.disconnectFrom(store, updated); - } else { - updated = store.entities().patch(instance, draft); - } - - ThingCommandResult relationshipResult = - connectRelationshipReferences(updated, relationships, true, false); - if (relationshipResult.isError()) { - rollbackAmendment(updated, instance, originalRelationships); - return relationshipResult; - } - - if (replaceExistingFieldsAndRelationships) { - originalRelationships.deleteFormerDependentsMadeInvalidBy(store, updated); - } - - return ThingCommandResult.success(updated); - } catch (Exception e) { - if (updated != null) { - rollbackAmendment(updated, instance, originalRelationships); - } - return ThingCommandResult.error(messageFrom(e)); - } - } - - public ThingCommandResult delete(final EntityInstance instance) { - try { - store.entities().delete(instance); - return ThingCommandResult.success(); - } catch (Exception e) { - return ThingCommandResult.error(messageFrom(e)); - } - } - - public ThingCommandResult connectRelationship( - final EntityInstance parent, - final String relationshipName, - final EntityInstance child, - final boolean deleteChildOnRollback) { - boolean alreadyConnected = - isRelated(parent, new RelationshipConnection(relationshipName, child)); - try { - store.relationships().connect(parent, relationshipName, child); - - ValidationReport validNow = store.relationships().validate(child); - if (!validNow.isValid()) { - if (!alreadyConnected) { - store.relationships().disconnectBetween(parent, child, relationshipName); - } - if (deleteChildOnRollback) { - rollbackDelete(child); - } - return ThingCommandResult.error(validNow.getErrorMessages()); - } - - return ThingCommandResult.success(child); - } catch (Exception e) { - if (!alreadyConnected) { - store.relationships().disconnectBetween(parent, child, relationshipName); - } - if (deleteChildOnRollback) { - rollbackDelete(child); - } - return ThingCommandResult.error(messageFrom(e)); - } - } - - public ThingCommandResult disconnectRelationship( - final EntityInstance parent, - final EntityInstance child, - final String relationshipName) { - try { - store.relationships().removeBetween(parent, child, relationshipName); - return ThingCommandResult.success(); - } catch (Exception e) { - return ThingCommandResult.error(messageFrom(e)); - } - } - - public ThingCommandResult connectRelationships( - final EntityInstance instance, final List relationships) { - return connectRelationshipReferences(instance, relationships, true, false); - } - - private ThingCommandResult createAndConnect(final CreateAndConnectRelationshipCommand command) { - ThingCommandResult createResult = - create(command.getChildDraft(), command.getChildRelationships(), false); - if (createResult.isError()) { - return createResult; + return deleteHandler.handle((DeleteThingCommand) command); } - ThingCommandResult connectResult = - connectRelationship( - command.getParent(), - command.getRelationshipName(), - createResult.getInstance(), - true); - if (connectResult.isError()) { - return connectResult.withRolledBackCreatedInstance(); + if (command instanceof ReplaceThingCommand) { + return amendHandler.handle((ReplaceThingCommand) command); } - return ThingCommandResult.success(createResult.getInstance()); - } - - private ThingCommandResult connectRelationships( - final EntityInstance instance, - final List relationships, - final boolean validateFinalRelationships) { - List connectedByCommand = new ArrayList<>(); - try { - for (RelationshipConnection relationship : relationships) { - boolean alreadyConnected = isRelated(instance, relationship); - store.relationships() - .connect( - instance, - relationship.relationshipName(), - relationship.relatedInstance()); - if (!alreadyConnected) { - connectedByCommand.add(relationship); - } - } - - if (validateFinalRelationships) { - ValidationReport finalRelationships = store.relationships().validate(instance); - if (!finalRelationships.isValid()) { - disconnectConnections(instance, connectedByCommand); - return ThingCommandResult.error(finalRelationships.getErrorMessages()); - } - } - - return ThingCommandResult.success(instance); - } catch (Exception e) { - disconnectConnections(instance, connectedByCommand); - return ThingCommandResult.error("Error creating relationships " + messageFrom(e)); - } - } - - private ThingCommandResult connectRelationshipReferences( - final EntityInstance instance, - final List references, - final boolean validateFinalRelationships, - final boolean prefixRelationshipErrors) { - RelationshipResolution resolution = resolveRelationshipReferences(instance, references); - if (resolution.hasErrors()) { - if (prefixRelationshipErrors) { - return ThingCommandResult.error( - "Invalid relationships: " + String.join(", ", resolution.errors)); - } - return ThingCommandResult.error(resolution.errors); - } - - return connectRelationships(instance, resolution.relationships, validateFinalRelationships); - } - - private RelationshipResolution resolveRelationshipReferences( - final EntityInstance instance, final List references) { - List relationships = new ArrayList<>(); - List errors = new ArrayList<>(); - - for (RelationshipReference reference : references) { - EntityInstance related = resolveRelationshipReference(instance, reference); - if (related == null) { - errors.add(missingRelationshipReferenceMessage(reference)); - } else { - relationships.add( - new RelationshipConnection(reference.relationshipName(), related)); - } - } - - return new RelationshipResolution(relationships, errors); - } - - private EntityInstance resolveRelationshipReference( - final EntityInstance instance, final RelationshipReference reference) { - if (reference.hasExplicitTargetEntity()) { - return resolveExplicitRelationshipReference(reference); - } - - for (RelationshipVectorDefinition vector : - instance.getEntity().related().getRelationships(reference.relationshipName())) { - EntityDefinition relatedEntity = vector.getTo(); - EntityInstance related = - store.entityQueries() - .findByField( - relatedEntity, - reference.referenceFieldName(), - reference.referenceValue()); - if (related == null) { - related = - store.entityQueries() - .findByQueryIdentifier(relatedEntity, reference.referenceValue()); - } - if (related != null) { - return related; - } - } - - return null; - } - - private EntityInstance resolveExplicitRelationshipReference( - final RelationshipReference reference) { - EntityInstance related = - store.entityQueries() - .findByQueryIdentifier( - reference.targetEntity(), reference.referenceValue()); - if (related == null) { - related = - store.entityQueries() - .findByField( - reference.targetEntity(), - reference.referenceFieldName(), - reference.referenceValue()); - } - return related; - } - - private String missingRelationshipReferenceMessage(final RelationshipReference reference) { - if (reference.hasExplicitTargetEntity()) { - return String.format( - "cannot find %s of %s to relate to with %s %s", - reference.referenceFieldName(), - reference.targetTerm(), - reference.referenceFieldName(), - reference.referenceValue()); - } - - return String.format( - "cannot find %s to relate to with %s %s", - reference.relationshipName(), - reference.referenceFieldName(), - reference.referenceValue()); - } - - private void rollbackAmendment( - final EntityInstance current, - final EntityInstance original, - final RelationshipSnapshot originalRelationships) { - RelationshipSnapshot.capture(store, current).disconnectFrom(store, current); - EntityInstance restored = store.entities().replace(current, draftFrom(original)); - originalRelationships.restoreTo(store, restored); - } - - private void rollbackDelete(final EntityInstance instance) { - if (instance != null) { - store.entities().delete(instance); - } - } - - private void disconnectConnections( - final EntityInstance instance, final List relationships) { - for (RelationshipConnection relationship : relationships) { - store.relationships() - .disconnectBetween( - instance, - relationship.relatedInstance(), - relationship.relationshipName()); - } - } - - private boolean isRelated( - final EntityInstance instance, final RelationshipConnection relationship) { - for (EntityInstance related : - store.relationships().listRelated(instance, relationship.relationshipName())) { - if (related.getInternalId().equals(relationship.relatedInstance().getInternalId())) { - return true; - } - } - return false; - } - - private EntityInstanceDraft draftFrom(final EntityInstance instance) { - EntityInstanceDraft draft = EntityInstanceDraft.forEntity(instance.getEntity()); - for (FieldValue value : instance.getAssignedFieldValues()) { - Field field = instance.getEntity().getField(value.getName()); - if (field.getType() == FieldType.AUTO_INCREMENT - || field.getType() == FieldType.AUTO_GUID) { - draft.withProtectedField(value.getName(), value.asString()); - } else { - draft.withField(value.getName(), value.asString()); - } - } - return draft; - } - - private String messageFrom(final Exception exception) { - String message = exception.getMessage(); - return message == null ? "" : message; - } - - private static final class RelationshipResolution { - - private final List relationships; - private final List errors; - - private RelationshipResolution( - final List relationships, final List errors) { - this.relationships = relationships; - this.errors = errors; - } - - private boolean hasErrors() { - return !errors.isEmpty(); - } - } - - private static final class RelationshipSnapshot { - - private final List links; - - private RelationshipSnapshot(final List links) { - this.links = links; - } - - private static RelationshipSnapshot capture( - final ThingStore store, final EntityInstance instance) { - List links = new ArrayList<>(); - Set seenLinks = new HashSet<>(); - for (RelationshipVectorDefinition vector : - instance.getEntity().related().getRelationships()) { - for (EntityInstance related : - store.relationships().listRelated(instance, vector.getName())) { - String key = vector.getName() + "|" + related.getInternalId(); - if (seenLinks.add(key)) { - links.add( - new RelationshipLink( - vector.getName(), - related, - store.relationships().validate(related).isValid())); - } - } - } - return new RelationshipSnapshot(links); + if (command instanceof ConnectExistingRelationshipCommand) { + return relationshipHandler.handle((ConnectExistingRelationshipCommand) command); } - private void disconnectFrom(final ThingStore store, final EntityInstance instance) { - for (RelationshipLink link : links) { - store.relationships() - .disconnectBetween(instance, link.related, link.relationshipName); - } + if (command instanceof CreateAndConnectRelationshipCommand) { + return relationshipHandler.handle((CreateAndConnectRelationshipCommand) command); } - private void restoreTo(final ThingStore store, final EntityInstance instance) { - for (RelationshipLink link : links) { - store.relationships().connect(instance, link.relationshipName, link.related); - } + if (command instanceof RelateThingCommand) { + return relationshipHandler.handle((RelateThingCommand) command); } - private void deleteFormerDependentsMadeInvalidBy( - final ThingStore store, final EntityInstance instance) { - for (RelationshipLink link : links) { - if (link.relatedWasValid - && !link.isStillRelatedTo(store, instance) - && !store.relationships().validate(link.related).isValid()) { - store.entities().delete(link.related); - } - } - } - } - - private static final class RelationshipLink { - - private final String relationshipName; - private final EntityInstance related; - private final boolean relatedWasValid; - - private RelationshipLink( - final String relationshipName, - final EntityInstance related, - final boolean relatedWasValid) { - this.relationshipName = relationshipName; - this.related = related; - this.relatedWasValid = relatedWasValid; + if (command instanceof DisconnectRelationshipCommand) { + return relationshipHandler.handle((DisconnectRelationshipCommand) command); } - private boolean isStillRelatedTo(final ThingStore store, final EntityInstance instance) { - for (EntityInstance current : - store.relationships().listRelated(instance, relationshipName)) { - if (current.getInternalId().equals(related.getInternalId())) { - return true; - } - } - return false; - } + return ThingCommandResult.error( + ApplicationError.unsupported( + String.format( + "Unsupported command %s", command.getClass().getSimpleName()))); } } diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingDefinitionResolver.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingDefinitionResolver.java new file mode 100644 index 00000000..af45a15c --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingDefinitionResolver.java @@ -0,0 +1,35 @@ +package uk.co.compendiumdev.thingifier.application; + +import uk.co.compendiumdev.thingifier.application.schema.SchemaDefinitionResolver; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; +import uk.co.compendiumdev.thingifier.core.repository.ThingStore; + +final class ThingDefinitionResolver { + + private final ThingStore store; + private final SchemaDefinitionResolver schema; + + ThingDefinitionResolver(final ThingStore store, final SchemaDefinitionResolver schema) { + this.store = store; + this.schema = schema; + } + + EntityDefinition entityNamed(final String entityName) { + if (entityName == null || entityName.isEmpty()) { + return null; + } + EntityDefinition entity = schema.entityNamed(entityName); + if (entity != null) { + return entity; + } + return schema.definitionWithSingularOrPluralName(entityName); + } + + EntityInstance resolveInstance(final EntityDefinition entity, final String queryIdentifier) { + if (entity == null) { + return null; + } + return store.entityQueries().findByQueryIdentifier(entity, queryIdentifier); + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingDraftFactory.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingDraftFactory.java new file mode 100644 index 00000000..59a42da3 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingDraftFactory.java @@ -0,0 +1,60 @@ +package uk.co.compendiumdev.thingifier.application; + +import java.util.ArrayList; +import java.util.List; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; +import uk.co.compendiumdev.thingifier.core.repository.ThingStore; + +final class ThingDraftFactory { + + private final ThingStore store; + + ThingDraftFactory(final ThingStore store) { + this.store = store; + } + + EntityInstanceDraft createDraft( + final EntityDefinition entity, + final String requestedPrimaryKey, + final List fieldValues) { + EntityInstanceDraft baseDraft = EntityInstanceDraft.forEntity(entity); + if (requestedPrimaryKey != null + && !requestedPrimaryKey.isEmpty() + && entity.hasPrimaryKeyField()) { + Field primaryKeyField = entity.getPrimaryKeyField(); + if (primaryKeyField.getType() == FieldType.AUTO_INCREMENT + || primaryKeyField.getType() == FieldType.AUTO_GUID) { + baseDraft.withProtectedField(primaryKeyField.getName(), requestedPrimaryKey); + } else { + baseDraft.withField(primaryKeyField.getName(), requestedPrimaryKey); + } + } + + List values = new ArrayList<>(fieldValues); + if (requestedPrimaryKey != null && !requestedPrimaryKey.isEmpty()) { + store.administration().accommodateProtectedIds(entity, values); + EntityInstanceDraft draft = + new EntityInstanceDraftBuilder(entity) + .overrideFieldValuesFromArgsIgnoring( + values, entity.getFieldNamesOfType(FieldType.AUTO_GUID)); + copyBaseDraftValues(baseDraft, draft); + return draft; + } + + return new EntityInstanceDraftBuilder(entity).setFieldValuesFrom(values); + } + + private void copyBaseDraftValues( + final EntityInstanceDraft baseDraft, final EntityInstanceDraft draft) { + for (NamedValue value : baseDraft.getFieldValues()) { + draft.withField(value.getName(), value.asString()); + } + for (NamedValue protectedValue : baseDraft.getProtectedFieldValues()) { + draft.withProtectedField(protectedValue.getName(), protectedValue.asString()); + } + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingQueryService.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingQueryService.java index 55dd333e..df1fb8f0 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingQueryService.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingQueryService.java @@ -4,6 +4,8 @@ import uk.co.compendiumdev.thingifier.application.query.ReadInstanceQuery; import uk.co.compendiumdev.thingifier.application.query.ReadRelationshipQuery; import uk.co.compendiumdev.thingifier.application.query.ThingReadQuery; +import uk.co.compendiumdev.thingifier.application.schema.SchemaDefinitionResolver; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; import uk.co.compendiumdev.thingifier.core.query.RepositoryQuery; import uk.co.compendiumdev.thingifier.core.query.RepositoryQueryResult; import uk.co.compendiumdev.thingifier.core.query.RepositoryQuerySpec; @@ -11,26 +13,31 @@ public final class ThingQueryService { + private final SchemaDefinitionResolver schema; + + public ThingQueryService(final SchemaDefinitionResolver schema) { + this.schema = schema; + } + public RepositoryQueryResult execute(final ThingReadQuery query, final ThingStore store) { return new RepositoryQuery(store, specFor(query)).performQuery(query.getQueryParams()); } private RepositoryQuerySpec specFor(final ThingReadQuery query) { + EntityDefinition entity = schema.entityNamed(query.getEntityName()); if (query instanceof ReadRelationshipQuery) { ReadRelationshipQuery relationship = (ReadRelationshipQuery) query; return RepositoryQuerySpec.relationship( - relationship.getEntity(), - relationship.getIdentifier(), - relationship.getRelationshipName()); + entity, relationship.getIdentifier(), relationship.getRelationshipName()); } if (query instanceof ReadInstanceQuery) { ReadInstanceQuery instance = (ReadInstanceQuery) query; - return RepositoryQuerySpec.instance(instance.getEntity(), instance.getIdentifier()); + return RepositoryQuerySpec.instance(entity, instance.getIdentifier()); } if (query instanceof ReadCollectionQuery) { - return RepositoryQuerySpec.collection(query.getEntity()); + return RepositoryQuerySpec.collection(entity); } throw new IllegalArgumentException( diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/WriteTransactionRunner.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/WriteTransactionRunner.java new file mode 100644 index 00000000..408d7646 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/WriteTransactionRunner.java @@ -0,0 +1,33 @@ +package uk.co.compendiumdev.thingifier.application; + +import java.util.function.Supplier; +import uk.co.compendiumdev.thingifier.core.repository.ThingStore; +import uk.co.compendiumdev.thingifier.core.repository.ThingStoreTransaction; + +final class WriteTransactionRunner { + + private final ThingStore store; + + WriteTransactionRunner(final ThingStore store) { + this.store = store; + } + + ThingCommandResult run(final Supplier operation) { + try (ThingStoreTransaction transaction = store.beginTransaction()) { + ThingCommandResult result = operation.get(); + if (result.isSuccessful()) { + transaction.commit(); + } else { + transaction.rollback(); + } + return result; + } catch (Exception e) { + return ThingCommandResult.error(messageFrom(e)); + } + } + + private String messageFrom(final Exception exception) { + String message = exception.getMessage(); + return message == null ? "" : message; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/WriteValidationPolicy.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/WriteValidationPolicy.java new file mode 100644 index 00000000..dd5bbbdd --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/WriteValidationPolicy.java @@ -0,0 +1,228 @@ +package uk.co.compendiumdev.thingifier.application; + +import java.util.ArrayList; +import java.util.List; +import uk.co.compendiumdev.thingifier.application.command.BodyFieldValue; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; +import uk.co.compendiumdev.thingifier.core.repository.ThingStore; + +final class WriteValidationPolicy { + + private final ThingStore store; + private final boolean enforceDeclaredTypes; + + WriteValidationPolicy(final ThingStore store, final boolean enforceDeclaredTypes) { + this.store = store; + this.enforceDeclaredTypes = enforceDeclaredTypes; + } + + ThingCommandResult validateDeclaredFieldTypes( + final EntityDefinition entity, final List bodyFields) { + return validateDeclaredFieldTypesIgnoring(entity, bodyFields, new ArrayList<>()); + } + + ThingCommandResult validateDeclaredFieldTypesIgnoringProtected( + final EntityDefinition entity, final List bodyFields) { + if (entity == null) { + return null; + } + return validateDeclaredFieldTypesIgnoring( + entity, + bodyFields, + entity.getFieldNamesOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID)); + } + + List normalizedFieldValues( + final EntityDefinition entity, + final List fieldValues, + final List bodyFields) { + if (entity == null) { + return fieldValues; + } + + List normalized = new ArrayList<>(); + for (NamedValue value : fieldValues) { + BodyFieldValue bodyField = bodyFieldNamed(bodyFields, value.getName()); + Field field = entity.getField(value.getName()); + if (bodyField != null + && field != null + && (field.getType() == FieldType.INTEGER + || field.getType() == FieldType.AUTO_INCREMENT) + && bodyField.getSourceType() == BodyFieldValue.SourceType.NUMERIC) { + normalized.add( + new NamedValue(value.getName(), integerString(bodyField.getValue()))); + } else { + normalized.add(value); + } + } + return normalized; + } + + ThingCommandResult validateCreate( + final EntityDefinition entity, + final List fieldValues, + final boolean allowRequestedPrimaryKey, + final String requestedPrimaryKey) { + List protectedFieldErrors = new ArrayList<>(); + for (String protectedField : + entity.getFieldNamesOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID)) { + if (containsField(fieldValues, protectedField) && !allowRequestedPrimaryKey) { + protectedFieldErrors.add( + String.format("Not allowed to create with %s", protectedField)); + } + } + if (!protectedFieldErrors.isEmpty()) { + return ThingCommandResult.error( + "Invalid Creation: " + String.join(", ", protectedFieldErrors)); + } + + if (allowRequestedPrimaryKey) { + ThingCommandResult duplicateResult = + duplicateProtectedFieldError(entity, fieldValues, requestedPrimaryKey); + if (duplicateResult != null) { + return duplicateResult; + } + } + return null; + } + + ThingCommandResult validateReplaceCreate( + final EntityDefinition entity, + final String identifier, + final List fieldValues) { + List generatedCreationFields = + entity.getFieldsOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID); + if (!generatedCreationFields.isEmpty()) { + return ThingCommandResult.error( + ApplicationError.replaceCreateAutoFieldsNotAllowed( + entity.getName(), fieldNames(generatedCreationFields))); + } + + Field primaryKey = entity.getPrimaryKeyField(); + for (NamedValue namedValue : fieldValues) { + if (namedValue.name.equals(primaryKey.getName()) + && !namedValue.value.equals(identifier)) { + return ThingCommandResult.error( + ApplicationError.replaceCreateKeyMismatch( + entity.getName(), identifier, namedValue.value)); + } + } + + return null; + } + + private ThingCommandResult validateDeclaredFieldTypesIgnoring( + final EntityDefinition entity, + final List bodyFields, + final List doNotValidateFields) { + if (!enforceDeclaredTypes || entity == null) { + return null; + } + + List errors = new ArrayList<>(); + for (BodyFieldValue fieldValue : bodyFields) { + if (doNotValidateFields.contains(fieldValue.getName())) { + continue; + } + + Field field = entity.getField(fieldValue.getName()); + if (field == null) { + continue; + } + + String errorMessage = + String.format( + "%s should be %s but was %s", + field.getName(), field.getType(), fieldValue.sourceTypeDisplayName()); + + if (field.getType() == FieldType.BOOLEAN + && fieldValue.getSourceType() != BodyFieldValue.SourceType.BOOLEAN) { + errors.add(errorMessage); + } + if ((field.getType() == FieldType.INTEGER + || field.getType() == FieldType.AUTO_INCREMENT) + && fieldValue.getSourceType() != BodyFieldValue.SourceType.NUMERIC) { + errors.add(errorMessage); + } + if (field.getType() == FieldType.FLOAT + && fieldValue.getSourceType() != BodyFieldValue.SourceType.NUMERIC) { + errors.add(errorMessage); + } + } + + if (errors.isEmpty()) { + return null; + } + return ThingCommandResult.error(String.join(", ", errors)); + } + + private ThingCommandResult duplicateProtectedFieldError( + final EntityDefinition entity, + final List fieldValues, + final String requestedPrimaryKey) { + for (String fieldName : + entity.getFieldNamesOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID)) { + String value = valueFor(fieldValues, fieldName); + if ((value == null || value.trim().isEmpty()) + && entity.hasPrimaryKeyField() + && entity.getPrimaryKeyField().getName().equals(fieldName)) { + value = requestedPrimaryKey; + } + if (value == null || value.trim().isEmpty()) { + continue; + } + EntityInstance found = store.entityQueries().findByField(entity, fieldName, value); + if (found != null) { + return ThingCommandResult.error( + ApplicationError.conflict( + "Cannot Create with duplicate values: " + + String.format( + "Found Existing item with %s of %s", + fieldName, value))); + } + } + return null; + } + + private boolean containsField(final List fieldValues, final String fieldName) { + return valueFor(fieldValues, fieldName) != null; + } + + private String valueFor(final List fieldValues, final String fieldName) { + for (NamedValue value : fieldValues) { + if (value.getName().equals(fieldName)) { + return value.asString(); + } + } + return null; + } + + private BodyFieldValue bodyFieldNamed( + final List bodyFields, final String fieldName) { + for (BodyFieldValue field : bodyFields) { + if (field.getName().equals(fieldName)) { + return field; + } + } + return null; + } + + private String integerString(final String value) { + return String.valueOf((int) Double.parseDouble(value)); + } + + private String fieldNames(final List fields) { + String names = ""; + for (Field field : fields) { + if (!names.isEmpty()) { + names = names + ", "; + } + names = names + field.getName(); + } + return names; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/AmendThingCommand.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/AmendThingCommand.java index 715370bc..a8b97f4c 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/AmendThingCommand.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/AmendThingCommand.java @@ -3,33 +3,61 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; public final class AmendThingCommand implements ThingWriteCommand { - private final EntityInstance instance; - private final EntityInstanceDraft draft; + private final String entityName; + private final String identifier; + private final List fieldValues; + private final List bodyFields; private final boolean replaceExistingFieldsAndRelationships; private final List relationships; public AmendThingCommand( - final EntityInstance instance, - final EntityInstanceDraft draft, + final String entityName, + final String identifier, + final List fieldValues, final boolean replaceExistingFieldsAndRelationships, final List relationships) { - this.instance = instance; - this.draft = draft; + this( + entityName, + identifier, + fieldValues, + BodyFieldValue.fromNamedValues(fieldValues), + replaceExistingFieldsAndRelationships, + relationships); + } + + public AmendThingCommand( + final String entityName, + final String identifier, + final List fieldValues, + final List bodyFields, + final boolean replaceExistingFieldsAndRelationships, + final List relationships) { + this.entityName = entityName; + this.identifier = identifier; + this.fieldValues = Collections.unmodifiableList(new ArrayList<>(fieldValues)); + this.bodyFields = Collections.unmodifiableList(new ArrayList<>(bodyFields)); this.replaceExistingFieldsAndRelationships = replaceExistingFieldsAndRelationships; this.relationships = Collections.unmodifiableList(new ArrayList<>(relationships)); } - public EntityInstance getInstance() { - return instance; + public String getEntityName() { + return entityName; + } + + public String getIdentifier() { + return identifier; + } + + public List getFieldValues() { + return fieldValues; } - public EntityInstanceDraft getDraft() { - return draft; + public List getBodyFields() { + return bodyFields; } public boolean shouldReplaceExistingFieldsAndRelationships() { diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/BodyFieldValue.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/BodyFieldValue.java new file mode 100644 index 00000000..cb9c7844 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/BodyFieldValue.java @@ -0,0 +1,62 @@ +package uk.co.compendiumdev.thingifier.application.command; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; + +public final class BodyFieldValue { + + public enum SourceType { + STRING("STRING"), + BOOLEAN("BOOLEAN"), + INTEGER("INTEGER"), + NUMERIC("NUMERIC"), + SOMETHING_ELSE("Something Else"); + + private final String displayName; + + SourceType(final String displayName) { + this.displayName = displayName; + } + + public String displayName() { + return displayName; + } + } + + private final String name; + private final String value; + private final SourceType sourceType; + + public BodyFieldValue(final String name, final String value, final SourceType sourceType) { + this.name = name; + this.value = value; + this.sourceType = sourceType == null ? SourceType.SOMETHING_ELSE : sourceType; + } + + public static List fromNamedValues(final List values) { + List bodyFields = new ArrayList<>(); + for (NamedValue value : values) { + bodyFields.add( + new BodyFieldValue(value.getName(), value.asString(), SourceType.STRING)); + } + return Collections.unmodifiableList(bodyFields); + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public SourceType getSourceType() { + return sourceType; + } + + public String sourceTypeDisplayName() { + return sourceType.displayName(); + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/ConnectExistingRelationshipCommand.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/ConnectExistingRelationshipCommand.java index 7e467578..96e60b01 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/ConnectExistingRelationshipCommand.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/ConnectExistingRelationshipCommand.java @@ -1,31 +1,42 @@ package uk.co.compendiumdev.thingifier.application.command; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; public final class ConnectExistingRelationshipCommand implements ThingWriteCommand { - private final EntityInstance parent; + private final String parentEntityName; + private final String parentIdentifier; private final String relationshipName; - private final EntityInstance child; + private final List childReferenceFields; public ConnectExistingRelationshipCommand( - final EntityInstance parent, + final String parentEntityName, + final String parentIdentifier, final String relationshipName, - final EntityInstance child) { - this.parent = parent; + final List childReferenceFields) { + this.parentEntityName = parentEntityName; + this.parentIdentifier = parentIdentifier; this.relationshipName = relationshipName; - this.child = child; + this.childReferenceFields = + Collections.unmodifiableList(new ArrayList<>(childReferenceFields)); } - public EntityInstance getParent() { - return parent; + public String getParentEntityName() { + return parentEntityName; + } + + public String getParentIdentifier() { + return parentIdentifier; } public String getRelationshipName() { return relationshipName; } - public EntityInstance getChild() { - return child; + public List getChildReferenceFields() { + return childReferenceFields; } } diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/CreateAndConnectRelationshipCommand.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/CreateAndConnectRelationshipCommand.java index 2cdae769..3142e2e1 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/CreateAndConnectRelationshipCommand.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/CreateAndConnectRelationshipCommand.java @@ -3,37 +3,74 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; public final class CreateAndConnectRelationshipCommand implements ThingWriteCommand { - private final EntityInstance parent; + private final String parentEntityName; + private final String parentIdentifier; private final String relationshipName; - private final EntityInstanceDraft childDraft; + private final String childEntityName; + private final List childFieldValues; + private final List childBodyFields; private final List childRelationships; public CreateAndConnectRelationshipCommand( - final EntityInstance parent, + final String parentEntityName, + final String parentIdentifier, final String relationshipName, - final EntityInstanceDraft childDraft, + final String childEntityName, + final List childFieldValues, final List childRelationships) { - this.parent = parent; + this( + parentEntityName, + parentIdentifier, + relationshipName, + childEntityName, + childFieldValues, + BodyFieldValue.fromNamedValues(childFieldValues), + childRelationships); + } + + public CreateAndConnectRelationshipCommand( + final String parentEntityName, + final String parentIdentifier, + final String relationshipName, + final String childEntityName, + final List childFieldValues, + final List childBodyFields, + final List childRelationships) { + this.parentEntityName = parentEntityName; + this.parentIdentifier = parentIdentifier; this.relationshipName = relationshipName; - this.childDraft = childDraft; + this.childEntityName = childEntityName; + this.childFieldValues = Collections.unmodifiableList(new ArrayList<>(childFieldValues)); + this.childBodyFields = Collections.unmodifiableList(new ArrayList<>(childBodyFields)); this.childRelationships = Collections.unmodifiableList(new ArrayList<>(childRelationships)); } - public EntityInstance getParent() { - return parent; + public String getParentEntityName() { + return parentEntityName; + } + + public String getParentIdentifier() { + return parentIdentifier; } public String getRelationshipName() { return relationshipName; } - public EntityInstanceDraft getChildDraft() { - return childDraft; + public String getChildEntityName() { + return childEntityName; + } + + public List getChildFieldValues() { + return childFieldValues; + } + + public List getChildBodyFields() { + return childBodyFields; } public List getChildRelationships() { diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/CreateThingCommand.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/CreateThingCommand.java index 141d3ff6..f4acbe69 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/CreateThingCommand.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/CreateThingCommand.java @@ -3,25 +3,79 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; public final class CreateThingCommand implements ThingWriteCommand { - private final EntityInstanceDraft draft; + private final String entityName; + private final String requestedPrimaryKey; + private final List fieldValues; + private final List bodyFields; private final List relationships; private final boolean validateFinalRelationships; public CreateThingCommand( - final EntityInstanceDraft draft, + final String entityName, + final List fieldValues, final List relationships, final boolean validateFinalRelationships) { - this.draft = draft; + this( + entityName, + "", + fieldValues, + BodyFieldValue.fromNamedValues(fieldValues), + relationships, + validateFinalRelationships); + } + + public CreateThingCommand( + final String entityName, + final String requestedPrimaryKey, + final List fieldValues, + final List relationships, + final boolean validateFinalRelationships) { + this( + entityName, + requestedPrimaryKey, + fieldValues, + BodyFieldValue.fromNamedValues(fieldValues), + relationships, + validateFinalRelationships); + } + + public CreateThingCommand( + final String entityName, + final String requestedPrimaryKey, + final List fieldValues, + final List bodyFields, + final List relationships, + final boolean validateFinalRelationships) { + this.entityName = entityName; + this.requestedPrimaryKey = requestedPrimaryKey == null ? "" : requestedPrimaryKey; + this.fieldValues = Collections.unmodifiableList(new ArrayList<>(fieldValues)); + this.bodyFields = Collections.unmodifiableList(new ArrayList<>(bodyFields)); this.relationships = Collections.unmodifiableList(new ArrayList<>(relationships)); this.validateFinalRelationships = validateFinalRelationships; } - public EntityInstanceDraft getDraft() { - return draft; + public String getEntityName() { + return entityName; + } + + public boolean hasRequestedPrimaryKey() { + return !requestedPrimaryKey.isEmpty(); + } + + public String getRequestedPrimaryKey() { + return requestedPrimaryKey; + } + + public List getFieldValues() { + return fieldValues; + } + + public List getBodyFields() { + return bodyFields; } public List getRelationships() { diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/DeleteThingCommand.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/DeleteThingCommand.java index 13aa27b2..29178cb3 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/DeleteThingCommand.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/DeleteThingCommand.java @@ -1,16 +1,20 @@ package uk.co.compendiumdev.thingifier.application.command; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; - public final class DeleteThingCommand implements ThingWriteCommand { - private final EntityInstance instance; + private final String entityName; + private final String identifier; + + public DeleteThingCommand(final String entityName, final String identifier) { + this.entityName = entityName; + this.identifier = identifier; + } - public DeleteThingCommand(final EntityInstance instance) { - this.instance = instance; + public String getEntityName() { + return entityName; } - public EntityInstance getInstance() { - return instance; + public String getIdentifier() { + return identifier; } } diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/DisconnectRelationshipCommand.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/DisconnectRelationshipCommand.java index ceb0bbce..84f996f0 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/DisconnectRelationshipCommand.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/DisconnectRelationshipCommand.java @@ -1,31 +1,36 @@ package uk.co.compendiumdev.thingifier.application.command; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; - public final class DisconnectRelationshipCommand implements ThingWriteCommand { - private final EntityInstance parent; - private final EntityInstance child; + private final String parentEntityName; + private final String parentIdentifier; private final String relationshipName; + private final String childIdentifier; public DisconnectRelationshipCommand( - final EntityInstance parent, - final EntityInstance child, - final String relationshipName) { - this.parent = parent; - this.child = child; + final String parentEntityName, + final String parentIdentifier, + final String relationshipName, + final String childIdentifier) { + this.parentEntityName = parentEntityName; + this.parentIdentifier = parentIdentifier; this.relationshipName = relationshipName; + this.childIdentifier = childIdentifier; } - public EntityInstance getParent() { - return parent; + public String getParentEntityName() { + return parentEntityName; } - public EntityInstance getChild() { - return child; + public String getParentIdentifier() { + return parentIdentifier; } public String getRelationshipName() { return relationshipName; } + + public String getChildIdentifier() { + return childIdentifier; + } } diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/RelateThingCommand.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/RelateThingCommand.java new file mode 100644 index 00000000..165633cc --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/RelateThingCommand.java @@ -0,0 +1,70 @@ +package uk.co.compendiumdev.thingifier.application.command; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; + +public final class RelateThingCommand implements ThingWriteCommand { + + private final String parentEntityName; + private final String parentIdentifier; + private final String relationshipName; + private final List bodyFieldValues; + private final List bodyFields; + private final List bodyRelationships; + + public RelateThingCommand( + final String parentEntityName, + final String parentIdentifier, + final String relationshipName, + final List bodyFieldValues, + final List bodyRelationships) { + this( + parentEntityName, + parentIdentifier, + relationshipName, + bodyFieldValues, + BodyFieldValue.fromNamedValues(bodyFieldValues), + bodyRelationships); + } + + public RelateThingCommand( + final String parentEntityName, + final String parentIdentifier, + final String relationshipName, + final List bodyFieldValues, + final List bodyFields, + final List bodyRelationships) { + this.parentEntityName = parentEntityName; + this.parentIdentifier = parentIdentifier; + this.relationshipName = relationshipName; + this.bodyFieldValues = Collections.unmodifiableList(new ArrayList<>(bodyFieldValues)); + this.bodyFields = Collections.unmodifiableList(new ArrayList<>(bodyFields)); + this.bodyRelationships = Collections.unmodifiableList(new ArrayList<>(bodyRelationships)); + } + + public String getParentEntityName() { + return parentEntityName; + } + + public String getParentIdentifier() { + return parentIdentifier; + } + + public String getRelationshipName() { + return relationshipName; + } + + public List getBodyFieldValues() { + return bodyFieldValues; + } + + public List getBodyFields() { + return bodyFields; + } + + public List getBodyRelationships() { + return bodyRelationships; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/RelationshipReference.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/RelationshipReference.java index db39203b..2832ecb7 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/RelationshipReference.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/RelationshipReference.java @@ -1,24 +1,22 @@ package uk.co.compendiumdev.thingifier.application.command; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; - public final class RelationshipReference { private final String relationshipName; - private final EntityDefinition targetEntity; + private final String targetEntityName; private final String targetTerm; private final String referenceFieldName; private final String referenceValue; private RelationshipReference( final String relationshipName, - final EntityDefinition targetEntity, + final String targetEntityName, final String targetTerm, final String referenceFieldName, final String referenceValue) { this.relationshipName = relationshipName; - this.targetEntity = targetEntity; - this.targetTerm = targetTerm; + this.targetEntityName = targetEntityName == null ? "" : targetEntityName; + this.targetTerm = targetTerm == null ? "" : targetTerm; this.referenceFieldName = referenceFieldName; this.referenceValue = referenceValue; } @@ -28,17 +26,17 @@ public static RelationshipReference compressed( final String referenceFieldName, final String referenceValue) { return new RelationshipReference( - relationshipName, null, "", referenceFieldName, referenceValue); + relationshipName, "", "", referenceFieldName, referenceValue); } public static RelationshipReference explicit( final String relationshipName, - final EntityDefinition targetEntity, + final String targetEntityName, final String targetTerm, final String referenceFieldName, final String referenceValue) { return new RelationshipReference( - relationshipName, targetEntity, targetTerm, referenceFieldName, referenceValue); + relationshipName, targetEntityName, targetTerm, referenceFieldName, referenceValue); } public String relationshipName() { @@ -46,11 +44,11 @@ public String relationshipName() { } public boolean hasExplicitTargetEntity() { - return targetEntity != null; + return !targetEntityName.isEmpty(); } - public EntityDefinition targetEntity() { - return targetEntity; + public String targetEntityName() { + return targetEntityName; } public String targetTerm() { diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/ReplaceThingCommand.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/ReplaceThingCommand.java new file mode 100644 index 00000000..40874731 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/ReplaceThingCommand.java @@ -0,0 +1,61 @@ +package uk.co.compendiumdev.thingifier.application.command; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; + +public final class ReplaceThingCommand implements ThingWriteCommand { + + private final String entityName; + private final String identifier; + private final List fieldValues; + private final List bodyFields; + private final List relationships; + + public ReplaceThingCommand( + final String entityName, + final String identifier, + final List fieldValues, + final List relationships) { + this( + entityName, + identifier, + fieldValues, + BodyFieldValue.fromNamedValues(fieldValues), + relationships); + } + + public ReplaceThingCommand( + final String entityName, + final String identifier, + final List fieldValues, + final List bodyFields, + final List relationships) { + this.entityName = entityName; + this.identifier = identifier; + this.fieldValues = Collections.unmodifiableList(new ArrayList<>(fieldValues)); + this.bodyFields = Collections.unmodifiableList(new ArrayList<>(bodyFields)); + this.relationships = Collections.unmodifiableList(new ArrayList<>(relationships)); + } + + public String getEntityName() { + return entityName; + } + + public String getIdentifier() { + return identifier; + } + + public List getFieldValues() { + return fieldValues; + } + + public List getBodyFields() { + return bodyFields; + } + + public List getRelationships() { + return relationships; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ReadCollectionQuery.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ReadCollectionQuery.java index 23fc6f58..26194fd1 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ReadCollectionQuery.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ReadCollectionQuery.java @@ -1,21 +1,20 @@ package uk.co.compendiumdev.thingifier.application.query; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; public final class ReadCollectionQuery implements ThingReadQuery { - private final EntityDefinition entity; + private final String entityName; private final QueryFilterParams queryParams; - public ReadCollectionQuery(final EntityDefinition entity, final QueryFilterParams queryParams) { - this.entity = entity; + public ReadCollectionQuery(final String entityName, final QueryFilterParams queryParams) { + this.entityName = entityName; this.queryParams = ThingReadQuery.copyOf(queryParams); } @Override - public EntityDefinition getEntity() { - return entity; + public String getEntityName() { + return entityName; } @Override diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ReadInstanceQuery.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ReadInstanceQuery.java index de941991..5d4d751c 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ReadInstanceQuery.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ReadInstanceQuery.java @@ -1,26 +1,23 @@ package uk.co.compendiumdev.thingifier.application.query; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; public final class ReadInstanceQuery implements ThingReadQuery { - private final EntityDefinition entity; + private final String entityName; private final String identifier; private final QueryFilterParams queryParams; public ReadInstanceQuery( - final EntityDefinition entity, - final String identifier, - final QueryFilterParams queryParams) { - this.entity = entity; + final String entityName, final String identifier, final QueryFilterParams queryParams) { + this.entityName = entityName; this.identifier = identifier; this.queryParams = ThingReadQuery.copyOf(queryParams); } @Override - public EntityDefinition getEntity() { - return entity; + public String getEntityName() { + return entityName; } public String getIdentifier() { diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ReadRelationshipQuery.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ReadRelationshipQuery.java index c1818790..6091a3eb 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ReadRelationshipQuery.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ReadRelationshipQuery.java @@ -1,29 +1,28 @@ package uk.co.compendiumdev.thingifier.application.query; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; public final class ReadRelationshipQuery implements ThingReadQuery { - private final EntityDefinition entity; + private final String entityName; private final String identifier; private final String relationshipName; private final QueryFilterParams queryParams; public ReadRelationshipQuery( - final EntityDefinition entity, + final String entityName, final String identifier, final String relationshipName, final QueryFilterParams queryParams) { - this.entity = entity; + this.entityName = entityName; this.identifier = identifier; this.relationshipName = relationshipName; this.queryParams = ThingReadQuery.copyOf(queryParams); } @Override - public EntityDefinition getEntity() { - return entity; + public String getEntityName() { + return entityName; } public String getIdentifier() { diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ThingReadQuery.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ThingReadQuery.java index da209abd..553b96d3 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ThingReadQuery.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/query/ThingReadQuery.java @@ -1,12 +1,11 @@ package uk.co.compendiumdev.thingifier.application.query; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; import uk.co.compendiumdev.thingifier.core.query.FilterBy; import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; public interface ThingReadQuery { - EntityDefinition getEntity(); + String getEntityName(); QueryFilterParams getQueryParams(); diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/EntityTypeRef.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/EntityTypeRef.java new file mode 100644 index 00000000..4ada3cd8 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/EntityTypeRef.java @@ -0,0 +1,64 @@ +package uk.co.compendiumdev.thingifier.application.schema; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class EntityTypeRef { + + private final String name; + private final String pluralName; + private final String primaryKeyFieldName; + private final List fields; + private final List relationships; + + public EntityTypeRef( + final String name, + final String pluralName, + final String primaryKeyFieldName, + final List fields, + final List relationships) { + this.name = name; + this.pluralName = pluralName; + this.primaryKeyFieldName = primaryKeyFieldName; + this.fields = Collections.unmodifiableList(new ArrayList<>(fields)); + this.relationships = Collections.unmodifiableList(new ArrayList<>(relationships)); + } + + public String name() { + return name; + } + + public String pluralName() { + return pluralName; + } + + public boolean hasPrimaryKeyField() { + return primaryKeyFieldName != null && !primaryKeyFieldName.isEmpty(); + } + + public String primaryKeyFieldName() { + return primaryKeyFieldName; + } + + public List fields() { + return fields; + } + + public List relationships() { + return relationships; + } + + public boolean hasRelationship(final String relationshipName) { + return relationships.stream().anyMatch(spec -> spec.name().equals(relationshipName)); + } + + public FieldSpec fieldNamed(final String fieldName) { + for (FieldSpec field : fields) { + if (field.name().equals(fieldName)) { + return field; + } + } + return null; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/FieldSpec.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/FieldSpec.java new file mode 100644 index 00000000..72fa5b2f --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/FieldSpec.java @@ -0,0 +1,32 @@ +package uk.co.compendiumdev.thingifier.application.schema; + +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; + +public final class FieldSpec { + + private final String name; + private final FieldType type; + private final boolean unique; + + public FieldSpec(final String name, final FieldType type, final boolean unique) { + this.name = name; + this.type = type; + this.unique = unique; + } + + public String name() { + return name; + } + + public FieldType type() { + return type; + } + + public boolean isUnique() { + return unique; + } + + public boolean isProtectedField() { + return type == FieldType.AUTO_INCREMENT || type == FieldType.AUTO_GUID; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/RelationshipSpec.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/RelationshipSpec.java new file mode 100644 index 00000000..87d97d61 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/RelationshipSpec.java @@ -0,0 +1,27 @@ +package uk.co.compendiumdev.thingifier.application.schema; + +public final class RelationshipSpec { + + private final String name; + private final String fromEntityName; + private final String toEntityName; + + public RelationshipSpec( + final String name, final String fromEntityName, final String toEntityName) { + this.name = name; + this.fromEntityName = fromEntityName; + this.toEntityName = toEntityName; + } + + public String name() { + return name; + } + + public String fromEntityName() { + return fromEntityName; + } + + public String toEntityName() { + return toEntityName; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/SchemaDefinitionResolver.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/SchemaDefinitionResolver.java new file mode 100644 index 00000000..d96ba8a9 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/SchemaDefinitionResolver.java @@ -0,0 +1,10 @@ +package uk.co.compendiumdev.thingifier.application.schema; + +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; + +public interface SchemaDefinitionResolver { + + EntityDefinition entityNamed(String name); + + EntityDefinition definitionWithSingularOrPluralName(String term); +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/SchemaViewCatalog.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/SchemaViewCatalog.java new file mode 100644 index 00000000..5ce49318 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/SchemaViewCatalog.java @@ -0,0 +1,8 @@ +package uk.co.compendiumdev.thingifier.application.schema; + +public interface SchemaViewCatalog { + + EntityTypeRef entityWithSingularOrPluralName(String term); + + boolean hasRelationshipNamed(String name); +} diff --git a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/ThingRouteMapperTest.java b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/ThingRouteMapperTest.java new file mode 100644 index 00000000..3273c200 --- /dev/null +++ b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/ThingRouteMapperTest.java @@ -0,0 +1,83 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import uk.co.compendiumdev.thingifier.Thingifier; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingifierSchemaCatalog; +import uk.co.compendiumdev.thingifier.core.domain.definitions.Cardinality; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; + +class ThingRouteMapperTest { + + @Test + void mapsCollectionRoute() { + ThingRoute route = mapper().map("/tasks/"); + + Assertions.assertTrue(route instanceof CollectionRoute); + Assertions.assertEquals("task", ((CollectionRoute) route).entity().name()); + } + + @Test + void mapsInstanceRoute() { + ThingRoute route = mapper().map("tasks/123"); + + Assertions.assertTrue(route instanceof InstanceRoute); + Assertions.assertEquals("123", ((InstanceRoute) route).identifier()); + } + + @Test + void mapsRelationshipCollectionRouteWithoutResolvingInstances() { + ThingRoute route = mapper().map("projects/p1/tasks"); + + Assertions.assertTrue(route instanceof RelationshipCollectionRoute); + RelationshipCollectionRoute relationship = (RelationshipCollectionRoute) route; + Assertions.assertEquals("project", relationship.parentEntity().name()); + Assertions.assertEquals("p1", relationship.parentIdentifier()); + Assertions.assertEquals("tasks", relationship.relationshipName()); + } + + @Test + void mapsRelationshipInstanceRouteWithoutResolvingInstances() { + ThingRoute route = mapper().map("projects/p1/tasks/t1"); + + Assertions.assertTrue(route instanceof RelationshipInstanceRoute); + RelationshipInstanceRoute relationship = (RelationshipInstanceRoute) route; + Assertions.assertEquals("p1", relationship.parentIdentifier()); + Assertions.assertEquals("t1", relationship.childIdentifier()); + } + + @Test + void rejectsUnknownRoot() { + ThingRoute route = mapper().map("widgets/1"); + + Assertions.assertTrue(route instanceof UnmatchedRoute); + Assertions.assertEquals("widgets", ((UnmatchedRoute) route).firstPart()); + } + + @Test + void rejectsUnknownRelationshipName() { + ThingRoute route = mapper().map("projects/p1/widgets"); + + Assertions.assertTrue(route instanceof UnmatchedRoute); + } + + private ThingRouteMapper mapper() { + return new ThingRouteMapper(new ThingifierSchemaCatalog(thingifier())); + } + + private Thingifier thingifier() { + Thingifier thingifier = new Thingifier(); + EntityDefinition task = thingifier.defineThing("task", "tasks"); + task.addAsPrimaryKeyField(Field.is("guid", FieldType.AUTO_GUID)); + + EntityDefinition project = thingifier.defineThing("project", "projects"); + project.addAsPrimaryKeyField(Field.is("guid", FieldType.AUTO_GUID)); + + thingifier + .defineRelationship(task, project, "task-of", Cardinality.ONE_TO_ONE()) + .whenReversed(Cardinality.ONE_TO_MANY(), "tasks"); + return thingifier; + } +} diff --git a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/http/ApiRequestEnvelopeTest.java b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/http/ApiRequestEnvelopeTest.java new file mode 100644 index 00000000..12ff8a6b --- /dev/null +++ b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/http/ApiRequestEnvelopeTest.java @@ -0,0 +1,47 @@ +package uk.co.compendiumdev.thingifier.api.http; + +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ApiRequestEnvelopeTest { + + @Test + public void getEnvelopeCarriesRouteHeadersAndQueryWithoutBodyParsing() { + HttpApiRequest request = + new HttpApiRequest("/tasks") + .addHeader("Accept", "application/json") + .setFilterableQueryParams("title=Task"); + + ApiRequestEnvelope envelope = + ApiRequestEnvelope.from( + request, ThingifierHttpApi.HttpVerb.GET, List.of("task", "tasks")); + + Assertions.assertEquals(ThingifierHttpApi.HttpVerb.GET, envelope.verb()); + Assertions.assertEquals("tasks", envelope.path()); + Assertions.assertEquals("application/json", envelope.headers().get("Accept")); + Assertions.assertEquals(1, envelope.queryParams().size()); + Assertions.assertTrue(envelope.bodyFields().asMap().isEmpty()); + } + + @Test + public void postEnvelopeParsesBodyIntoTypedBodyFieldsOnceAtTheEdge() { + HttpApiRequest request = + new HttpApiRequest("/tasks") + .setBody( + "{\"title\":\"Task\",\"done\":false,\"relationships\":{\"project\":{\"guid\":\"p1\"}}}"); + + ApiRequestEnvelope envelope = + ApiRequestEnvelope.from( + request, ThingifierHttpApi.HttpVerb.POST, List.of("task", "tasks")); + + Assertions.assertEquals("Task", envelope.bodyFields().asMap().get("title")); + Assertions.assertEquals("false", envelope.bodyFields().asStringMap().get("done")); + Assertions.assertTrue( + envelope.bodyFields().asFlattenedStringMap().stream() + .anyMatch( + entry -> + "relationships.project.guid".equals(entry.getKey()) + && "p1".equals(entry.getValue()))); + } +} diff --git a/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/query/urlparamsparser/FilterableParamsTest.java b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/http/UrlQueryParamParserTest.java similarity index 81% rename from ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/query/urlparamsparser/FilterableParamsTest.java rename to thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/http/UrlQueryParamParserTest.java index 92b2df78..3a58458b 100644 --- a/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/query/urlparamsparser/FilterableParamsTest.java +++ b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/http/UrlQueryParamParserTest.java @@ -1,4 +1,4 @@ -package uk.co.compendiumdev.thingifier.core.query.urlparamsparser; +package uk.co.compendiumdev.thingifier.api.http; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -6,9 +6,8 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; -import uk.co.compendiumdev.thingifier.core.query.fromurl.UrlParamParser; -public class FilterableParamsTest { +public class UrlQueryParamParserTest { /* The default Spark parsing for url params is a split by & and then a split by = @@ -21,16 +20,16 @@ This would come through as (id>,2) and (sortBy,-id) so we would lose the >= and @ParameterizedTest @CsvSource({"id%3E%3D4,id>=4", "id%3E=4,id>=4"}) - public void canProcessUrlEncoded(String encoded, String decoded) { + public void canProcessUrlEncoded(final String encoded, final String decoded) { - UrlParamParser parser = new UrlParamParser(); + UrlQueryParamParser parser = new UrlQueryParamParser(); String decodedValue = parser.urlDecode(encoded); Assertions.assertEquals(decoded, decodedValue); } @Test public void canSplitGreaterThanEqual() { - UrlParamParser parser = new UrlParamParser(); + UrlQueryParamParser parser = new UrlQueryParamParser(); QueryFilterParams values = parser.parse("id%3E%3D4"); Assertions.assertEquals("id", values.get(0).fieldName); Assertions.assertEquals(">=", values.get(0).filterOperation); @@ -39,7 +38,7 @@ public void canSplitGreaterThanEqual() { @Test public void canSplitIntoGreaterThanEqualAndLessThanEqual() { - UrlParamParser parser = new UrlParamParser(); + UrlQueryParamParser parser = new UrlQueryParamParser(); QueryFilterParams values = parser.parse("id%3E%3D4&id<=7"); Assertions.assertEquals(2, values.size()); @@ -56,7 +55,7 @@ public void canSplitIntoGreaterThanEqualAndLessThanEqual() { @ValueSource( strings = {"id%3E%3D4&", "id%3E%3D4& ", "id%3E%3D4&&&&", "id%3E%3D4&%20 && & "}) public void canHandleNullParams() { - UrlParamParser parser = new UrlParamParser(); + UrlQueryParamParser parser = new UrlQueryParamParser(); QueryFilterParams values = parser.parse("id%3E%3D4&"); Assertions.assertEquals(1, values.size()); @@ -67,22 +66,22 @@ public void canHandleNullParams() { @Test public void canHandleNullUrl() { - UrlParamParser parser = new UrlParamParser(); + UrlQueryParamParser parser = new UrlQueryParamParser(); QueryFilterParams values = parser.parse(null); Assertions.assertEquals(0, values.size()); } @ParameterizedTest @ValueSource(strings = {" ", " ", ""}) - public void canHandleEmptyStrings(String aString) { - UrlParamParser parser = new UrlParamParser(); + public void canHandleEmptyStrings(final String aString) { + UrlQueryParamParser parser = new UrlQueryParamParser(); QueryFilterParams values = parser.parse(aString); Assertions.assertEquals(0, values.size()); } @Test public void processPartialFilterWithoutOperator() { - UrlParamParser parser = new UrlParamParser(); + UrlQueryParamParser parser = new UrlQueryParamParser(); QueryFilterParams values = parser.parse("fieldname4"); Assertions.assertEquals(1, values.size()); @@ -93,7 +92,7 @@ public void processPartialFilterWithoutOperator() { @Test public void processPartialFilterWithoutValue() { - UrlParamParser parser = new UrlParamParser(); + UrlQueryParamParser parser = new UrlQueryParamParser(); QueryFilterParams values = parser.parse("fieldname>="); Assertions.assertEquals(1, values.size()); diff --git a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipBodyCommandParserTest.java b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipBodyCommandParserTest.java index 4ae02471..35715440 100644 --- a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipBodyCommandParserTest.java +++ b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipBodyCommandParserTest.java @@ -1,14 +1,17 @@ package uk.co.compendiumdev.thingifier.api.restapihandlers; -import com.google.gson.Gson; import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; -import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.RelationshipBodyCommandParser; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.RelationshipBodyCommands; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingReadRequestMapper; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingifierSchemaCatalog; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.ThingRouteMapper; +import uk.co.compendiumdev.thingifier.api.http.bodyparser.ApiBodyFields; import uk.co.compendiumdev.thingifier.application.command.RelationshipReference; import uk.co.compendiumdev.thingifier.core.EntityRelModel; import uk.co.compendiumdev.thingifier.core.domain.definitions.Cardinality; @@ -24,7 +27,7 @@ public class RelationshipBodyCommandParserTest { @Test public void parsesCompressedRelationshipAsValidationAndExecutionCommand() { Thingifier thingifier = taskProjectThingifier(); - EntityDefinition task = thingifier.getDefinitionNamed("task"); + ThingifierSchemaCatalog schema = new ThingifierSchemaCatalog(thingifier); EntityDefinition project = thingifier.getDefinitionNamed("project"); EntityInstance projectInstance = thingifier @@ -39,8 +42,10 @@ public void parsesCompressedRelationshipAsValidationAndExecutionCommand() { body.put("task-of.guid", projectInstance.getPrimaryKeyValue()); RelationshipBodyCommands commands = - new RelationshipBodyCommandParser(thingifier) - .parse(parserFor(thingifier, body), task); + new RelationshipBodyCommandParser(schema) + .parse( + bodyFieldsFor(body).asFlattenedStringMap(), + schema.entityWithSingularOrPluralName("task")); Assertions.assertTrue(commands.validationReport().isValid()); Assertions.assertEquals(1, commands.relationshipEntries().size()); @@ -65,18 +70,26 @@ public void routeParserRecognisesRelationshipRoutesAtRestBoundary() { EntityInstanceDraft.forEntity(project) .withField("title", "Project")); - ThingReadRequestMapper mapper = new ThingReadRequestMapper(thingifier); + ThingReadRequestMapper mapper = + new ThingReadRequestMapper(new ThingifierSchemaCatalog(thingifier)); + ThingifierSchemaCatalog schema = new ThingifierSchemaCatalog(thingifier); Assertions.assertFalse( mapper.map( - String.format( - "project/%s/tasks", projectInstance.getPrimaryKeyValue()), + new ThingRouteMapper(schema) + .map( + String.format( + "project/%s/tasks", + projectInstance.getPrimaryKeyValue())), new QueryFilterParams()) .isError()); Assertions.assertTrue( mapper.map( - String.format( - "project/%s/task", projectInstance.getPrimaryKeyValue()), + new ThingRouteMapper(schema) + .map( + String.format( + "project/%s/task", + projectInstance.getPrimaryKeyValue())), new QueryFilterParams()) .isError()); } @@ -97,8 +110,7 @@ private Thingifier taskProjectThingifier() { return thingifier; } - private BodyParser parserFor(final Thingifier thingifier, final Map body) { - HttpApiRequest request = new HttpApiRequest("/path").setBody(new Gson().toJson(body)); - return new BodyParser(request, thingifier.getThingNames()); + private ApiBodyFields bodyFieldsFor(final Map body) { + return ApiBodyFields.fromMap(new HashMap<>(body)); } } diff --git a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RepositoryBackedRelationshipUrlResolverTest.java b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RepositoryBackedRelationshipUrlResolverTest.java deleted file mode 100644 index 4103efee..00000000 --- a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RepositoryBackedRelationshipUrlResolverTest.java +++ /dev/null @@ -1,179 +0,0 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.core.EntityRelModel; -import uk.co.compendiumdev.thingifier.core.domain.definitions.Cardinality; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; -import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; -import uk.co.compendiumdev.thingifier.core.repository.ThingStore; - -public class RepositoryBackedRelationshipUrlResolverTest { - - private Thingifier thingifier; - private ThingStore repository; - private EntityDefinition todo; - private EntityDefinition project; - private EntityInstance task; - private EntityInstance projectInstance; - private RepositoryBackedRelationshipUrlResolver resolver; - - @BeforeEach - public void createModel() { - thingifier = new Thingifier(); - - todo = thingifier.defineThing("todo", "todos"); - todo.addAsPrimaryKeyField(Field.is("guid", FieldType.AUTO_GUID)); - todo.addField(Field.is("id", FieldType.AUTO_INCREMENT)); - todo.addField(Field.is("title", FieldType.STRING)); - - project = thingifier.defineThing("project", "projects"); - project.addAsPrimaryKeyField(Field.is("guid", FieldType.AUTO_GUID)); - project.addField(Field.is("id", FieldType.AUTO_INCREMENT)); - project.addField(Field.is("title", FieldType.STRING)); - - thingifier - .defineRelationship(project, todo, "tasks", Cardinality.ONE_TO_MANY()) - .whenReversed(Cardinality.ONE_TO_MANY(), "task-of"); - - repository = thingifier.getStore(EntityRelModel.DEFAULT_DATABASE_NAME); - task = - repository - .entities() - .create( - EntityInstanceDraft.forEntity(todo) - .withField("title", "relationship task")); - projectInstance = - repository - .entities() - .create( - EntityInstanceDraft.forEntity(project) - .withField("title", "relationship project")); - repository.relationships().connect(projectInstance, "tasks", task); - - resolver = - new RepositoryBackedRelationshipUrlResolver( - new ThingifierSchemaCatalog(thingifier), repository); - } - - @Test - public void resolvesRelationshipCollectionPathWithSingularEntityName() { - RepositoryBackedRelationshipUrlResolver.RelationshipUrlResolution resolution = - resolver.resolveCollection( - String.format("project/%s/tasks", projectInstance.getPrimaryKeyValue())); - - Assertions.assertTrue(resolution.matchedRelationshipPath()); - Assertions.assertFalse(resolution.relationshipInstancePath()); - Assertions.assertEquals(project, resolution.parentEntity()); - Assertions.assertEquals(projectInstance, resolution.parentInstance()); - Assertions.assertEquals("tasks", resolution.relationshipName()); - Assertions.assertNull(resolution.childInstance()); - } - - @Test - public void resolvesRelationshipCollectionPathWithPluralEntityName() { - RepositoryBackedRelationshipUrlResolver.RelationshipUrlResolution resolution = - resolver.resolveCollection( - String.format("projects/%s/tasks", projectInstance.getPrimaryKeyValue())); - - Assertions.assertTrue(resolution.matchedRelationshipPath()); - Assertions.assertEquals(projectInstance, resolution.parentInstance()); - Assertions.assertEquals("tasks", resolution.relationshipName()); - } - - @Test - public void resolvesReverseRelationshipCollectionPath() { - RepositoryBackedRelationshipUrlResolver.RelationshipUrlResolution resolution = - resolver.resolveCollection( - String.format("todo/%s/task-of", task.getPrimaryKeyValue())); - - Assertions.assertTrue(resolution.matchedRelationshipPath()); - Assertions.assertEquals(todo, resolution.parentEntity()); - Assertions.assertEquals(task, resolution.parentInstance()); - Assertions.assertEquals("task-of", resolution.relationshipName()); - } - - @Test - public void relationshipCollectionPathCanMatchEvenWhenParentInstanceDoesNotExist() { - RepositoryBackedRelationshipUrlResolver.RelationshipUrlResolution resolution = - resolver.resolveCollection("project/not-a-guid/tasks"); - - Assertions.assertTrue(resolution.matchedRelationshipPath()); - Assertions.assertEquals(project, resolution.parentEntity()); - Assertions.assertNull(resolution.parentInstance()); - Assertions.assertEquals("tasks", resolution.relationshipName()); - } - - @Test - public void doesNotMatchUnknownRelationshipForEntity() { - RepositoryBackedRelationshipUrlResolver.RelationshipUrlResolution resolution = - resolver.resolveCollection( - String.format("project/%s/unknown", projectInstance.getPrimaryKeyValue())); - - Assertions.assertFalse(resolution.matchedRelationshipPath()); - } - - @Test - public void resolvesRelationshipInstancePathByPrimaryKey() { - RepositoryBackedRelationshipUrlResolver.RelationshipUrlResolution resolution = - resolver.resolveRelationshipInstance( - String.format( - "project/%s/tasks/%s", - projectInstance.getPrimaryKeyValue(), task.getPrimaryKeyValue())); - - Assertions.assertTrue(resolution.matchedRelationshipPath()); - Assertions.assertTrue(resolution.relationshipInstancePath()); - Assertions.assertEquals(projectInstance, resolution.parentInstance()); - Assertions.assertEquals(task, resolution.childInstance()); - Assertions.assertEquals("tasks", resolution.relationshipName()); - } - - @Test - public void resolvesRelationshipInstancePathByAutoIncrementId() { - RepositoryBackedRelationshipUrlResolver.RelationshipUrlResolution resolution = - resolver.resolveRelationshipInstance( - String.format( - "project/%s/tasks/%s", - projectInstance.getPrimaryKeyValue(), - task.getFieldValue("id").asString())); - - Assertions.assertTrue(resolution.matchedRelationshipPath()); - Assertions.assertTrue(resolution.relationshipInstancePath()); - Assertions.assertEquals(task, resolution.childInstance()); - } - - @Test - public void relationshipInstancePathRequiresChildToBeConnected() { - EntityInstance unconnectedTask = - repository - .entities() - .create( - EntityInstanceDraft.forEntity(todo) - .withField("title", "unconnected task")); - - RepositoryBackedRelationshipUrlResolver.RelationshipUrlResolution resolution = - resolver.resolveRelationshipInstance( - String.format( - "project/%s/tasks/%s", - projectInstance.getPrimaryKeyValue(), - unconnectedTask.getPrimaryKeyValue())); - - Assertions.assertTrue(resolution.matchedRelationshipPath()); - Assertions.assertTrue(resolution.relationshipInstancePath()); - Assertions.assertNull(resolution.childInstance()); - } - - @Test - public void relationshipInstancePathDoesNotMatchCollectionOnlyUrl() { - RepositoryBackedRelationshipUrlResolver.RelationshipUrlResolution resolution = - resolver.resolveRelationshipInstance( - String.format("project/%s/tasks", projectInstance.getPrimaryKeyValue())); - - Assertions.assertFalse(resolution.matchedRelationshipPath()); - } -} diff --git a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingCommandResultApiMapperTest.java b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingCommandResultApiMapperTest.java new file mode 100644 index 00000000..86b1203a --- /dev/null +++ b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingCommandResultApiMapperTest.java @@ -0,0 +1,176 @@ +package uk.co.compendiumdev.thingifier.api.restapihandlers; + +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ApiRouteDisplay; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingCommandResultApiMapper; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingWriteRequestMapping; +import uk.co.compendiumdev.thingifier.api.response.ApiResponse; +import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; +import uk.co.compendiumdev.thingifier.application.ApplicationError; +import uk.co.compendiumdev.thingifier.application.ThingCommandResult; +import uk.co.compendiumdev.thingifier.application.command.CreateThingCommand; +import uk.co.compendiumdev.thingifier.application.command.DeleteThingCommand; +import uk.co.compendiumdev.thingifier.application.command.DisconnectRelationshipCommand; +import uk.co.compendiumdev.thingifier.application.command.RelateThingCommand; +import uk.co.compendiumdev.thingifier.application.command.ReplaceThingCommand; + +public class ThingCommandResultApiMapperTest { + + @Test + public void mapsReplaceCreateAutoFieldErrorToLegacyPutMessage() { + ThingCommandResult result = + ThingCommandResult.error( + ApplicationError.replaceCreateAutoFieldsNotAllowed("entity", "id")); + + ApiResponse response = + mapper().map(new ReplaceThingCommand("entity", "1", List.of(), List.of()), result); + + Assertions.assertEquals(400, response.getStatusCode()); + Assertions.assertEquals( + List.of("Cannot create entity with PUT due to Auto fields id"), + response.getErrorMessages()); + } + + @Test + public void mapsReplaceCreateKeyMismatchErrorToLegacyPutMessage() { + ThingCommandResult result = + ThingCommandResult.error( + ApplicationError.replaceCreateKeyMismatch("entity", "newkey", "innerkey")); + + ApiResponse response = + mapper().map( + new ReplaceThingCommand("entity", "newkey", List.of(), List.of()), + result); + + Assertions.assertEquals(400, response.getStatusCode()); + Assertions.assertEquals( + List.of( + "Cannot create entity with PUT as key does not match body value " + + "newkey != innerkey"), + response.getErrorMessages()); + } + + @Test + public void mapsConflictWithoutValidationPrefix() { + ThingCommandResult result = + ThingCommandResult.error( + ApplicationError.conflict( + "Cannot Create with duplicate values: Found Existing item " + + "with id of 1")); + + ApiResponse response = + mapper().map(new CreateThingCommand("entity", List.of(), List.of(), true), result); + + Assertions.assertEquals(409, response.getStatusCode()); + Assertions.assertEquals( + List.of("Cannot Create with duplicate values: Found Existing item with id of 1"), + response.getErrorMessages()); + } + + @Test + public void mapsApplicationCategoriesToHttpStatuses() { + Assertions.assertEquals(400, ThingCommandResultApiMapper.statusFor(null)); + Assertions.assertEquals( + 400, ThingCommandResultApiMapper.statusFor(ApplicationError.validation("bad"))); + Assertions.assertEquals( + 404, ThingCommandResultApiMapper.statusFor(ApplicationError.notFound("missing"))); + Assertions.assertEquals( + 409, ThingCommandResultApiMapper.statusFor(ApplicationError.conflict("dupe"))); + Assertions.assertEquals( + 400, + ThingCommandResultApiMapper.statusFor(ApplicationError.unsupported("unknown"))); + } + + @Test + public void mapsInstanceNotFoundWithRouteContextToLegacyPathMessage() { + ThingCommandResult result = + ThingCommandResult.error(ApplicationError.instanceNotFound("task", "missing")); + + ApiResponse response = + mapper().map( + ThingWriteRequestMapping.command( + new DeleteThingCommand("task", "missing"), + ApiRouteDisplay.originalPath("tasks/missing")), + result); + + Assertions.assertEquals(404, response.getStatusCode()); + Assertions.assertEquals( + List.of("Could not find any instances with tasks/missing"), + response.getErrorMessages()); + } + + @Test + public void mapsPostInstanceNotFoundWithAdapterMessage() { + ThingCommandResult result = + ThingCommandResult.error(ApplicationError.instanceNotFound("task", "missing")); + + ApiResponse response = + mapper().map( + ThingWriteRequestMapping.command( + new DeleteThingCommand("task", "missing"), + ApiRouteDisplay.missingInstanceMessage( + "No such task entity instance with guid == missing found")), + result); + + Assertions.assertEquals(404, response.getStatusCode()); + Assertions.assertEquals( + List.of("No such task entity instance with guid == missing found"), + response.getErrorMessages()); + } + + @Test + public void mapsMissingRelationshipParentToLegacyRouteMessage() { + ThingCommandResult result = + ThingCommandResult.error( + ApplicationError.parentInstanceNotFound( + "project", "missing-project", "tasks")); + + ApiResponse response = + mapper().map( + ThingWriteRequestMapping.command( + new RelateThingCommand( + "project", + "missing-project", + "tasks", + List.of(), + List.of()), + ApiRouteDisplay.originalPath( + "projects/missing-project/tasks")), + result); + + Assertions.assertEquals(404, response.getStatusCode()); + Assertions.assertEquals( + List.of( + "Could not find parent thing for relationship " + + "projects/missing-project/tasks"), + response.getErrorMessages()); + } + + @Test + public void mapsMissingRelationshipTargetToLegacyRouteMessage() { + ThingCommandResult result = + ThingCommandResult.error( + ApplicationError.relationshipTargetNotFound( + "project", "project-1", "tasks", "missing-task")); + + ApiResponse response = + mapper().map( + ThingWriteRequestMapping.command( + new DisconnectRelationshipCommand( + "project", "project-1", "tasks", "missing-task"), + ApiRouteDisplay.originalPath( + "projects/project-1/tasks/missing-task")), + result); + + Assertions.assertEquals(404, response.getStatusCode()); + Assertions.assertEquals( + List.of("Could not find any instances with projects/project-1/tasks/missing-task"), + response.getErrorMessages()); + } + + private ThingCommandResultApiMapper mapper() { + return new ThingCommandResultApiMapper(new ThingifierApiConfig("")); + } +} diff --git a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingReadRequestMapperTest.java b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingReadRequestMapperTest.java index 1763abdf..a2db5a4d 100644 --- a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingReadRequestMapperTest.java +++ b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingReadRequestMapperTest.java @@ -4,6 +4,10 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import uk.co.compendiumdev.thingifier.Thingifier; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingReadRequestMapper; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingReadRequestMapping; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingifierSchemaCatalog; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.ThingRouteMapper; import uk.co.compendiumdev.thingifier.application.query.ReadCollectionQuery; import uk.co.compendiumdev.thingifier.application.query.ReadInstanceQuery; import uk.co.compendiumdev.thingifier.application.query.ReadRelationshipQuery; @@ -19,19 +23,18 @@ public class ThingReadRequestMapperTest { public void mapsCollectionRoute() { Thingifier thingifier = taskProjectThingifier(); - ThingReadRequestMapping mapping = mapperFor(thingifier).map("task", params()); + ThingReadRequestMapping mapping = map(thingifier, "task", params()); Assertions.assertFalse(mapping.isError()); Assertions.assertTrue(mapping.getQuery() instanceof ReadCollectionQuery); - Assertions.assertEquals( - thingifier.getDefinitionNamed("task"), mapping.getQuery().getEntity()); + Assertions.assertEquals("task", mapping.getQuery().getEntityName()); } @Test public void mapsInstanceRoute() { Thingifier thingifier = taskProjectThingifier(); - ThingReadRequestMapping mapping = mapperFor(thingifier).map("task/abc", params()); + ThingReadRequestMapping mapping = map(thingifier, "task/abc", params()); Assertions.assertFalse(mapping.isError()); ReadInstanceQuery query = (ReadInstanceQuery) mapping.getQuery(); @@ -42,7 +45,7 @@ public void mapsInstanceRoute() { public void mapsRelationshipRoute() { Thingifier thingifier = taskProjectThingifier(); - ThingReadRequestMapping mapping = mapperFor(thingifier).map("project/p1/tasks", params()); + ThingReadRequestMapping mapping = map(thingifier, "project/p1/tasks", params()); Assertions.assertFalse(mapping.isError()); ReadRelationshipQuery query = (ReadRelationshipQuery) mapping.getQuery(); @@ -54,18 +57,16 @@ public void mapsRelationshipRoute() { public void mapsPluralEntityNames() { Thingifier thingifier = taskProjectThingifier(); - ThingReadRequestMapping mapping = mapperFor(thingifier).map("tasks", params()); + ThingReadRequestMapping mapping = map(thingifier, "tasks", params()); Assertions.assertFalse(mapping.isError()); Assertions.assertTrue(mapping.getQuery() instanceof ReadCollectionQuery); - Assertions.assertEquals( - thingifier.getDefinitionNamed("task"), mapping.getQuery().getEntity()); + Assertions.assertEquals("task", mapping.getQuery().getEntityName()); } @Test public void rejectsInvalidRoot() { - ThingReadRequestMapping mapping = - mapperFor(taskProjectThingifier()).map("missing", params()); + ThingReadRequestMapping mapping = map(taskProjectThingifier(), "missing", params()); Assertions.assertTrue(mapping.isError()); Assertions.assertEquals(404, mapping.getError().statusCode()); @@ -75,8 +76,7 @@ public void rejectsInvalidRoot() { @Test public void rejectsRelationshipNameInIdentifierPosition() { - ThingReadRequestMapping mapping = - mapperFor(taskProjectThingifier()).map("project/tasks", params()); + ThingReadRequestMapping mapping = map(taskProjectThingifier(), "project/tasks", params()); Assertions.assertTrue(mapping.isError()); Assertions.assertEquals(404, mapping.getError().statusCode()); @@ -84,8 +84,7 @@ public void rejectsRelationshipNameInIdentifierPosition() { @Test public void rejectsEntityNameInIdentifierPosition() { - ThingReadRequestMapping mapping = - mapperFor(taskProjectThingifier()).map("project/task", params()); + ThingReadRequestMapping mapping = map(taskProjectThingifier(), "project/task", params()); Assertions.assertTrue(mapping.isError()); Assertions.assertEquals(404, mapping.getError().statusCode()); @@ -93,7 +92,7 @@ public void rejectsEntityNameInIdentifierPosition() { @Test public void rejectsEmptyRoute() { - ThingReadRequestMapping mapping = mapperFor(taskProjectThingifier()).map("", params()); + ThingReadRequestMapping mapping = map(taskProjectThingifier(), "", params()); Assertions.assertTrue(mapping.isError()); Assertions.assertEquals(404, mapping.getError().statusCode()); @@ -102,7 +101,7 @@ public void rejectsEmptyRoute() { @Test public void rejectsTooDeepRoute() { ThingReadRequestMapping mapping = - mapperFor(taskProjectThingifier()).map("project/p1/tasks/t1", params()); + map(taskProjectThingifier(), "project/p1/tasks/t1", params()); Assertions.assertTrue(mapping.isError()); Assertions.assertEquals(404, mapping.getError().statusCode()); @@ -112,12 +111,11 @@ public void rejectsTooDeepRoute() { public void normalizesLeadingAndTrailingSlashes() { Thingifier thingifier = taskProjectThingifier(); - ThingReadRequestMapping mapping = mapperFor(thingifier).map("/projects/", params()); + ThingReadRequestMapping mapping = map(thingifier, "/projects/", params()); Assertions.assertFalse(mapping.isError()); Assertions.assertTrue(mapping.getQuery() instanceof ReadCollectionQuery); - Assertions.assertEquals( - thingifier.getDefinitionNamed("project"), mapping.getQuery().getEntity()); + Assertions.assertEquals("project", mapping.getQuery().getEntityName()); } @Test @@ -126,7 +124,7 @@ public void preservesQueryParamsOnMappedQuery() { QueryFilterParams params = new QueryFilterParams(); params.put("title", "=Task"); - ThingReadRequestMapping mapping = mapperFor(thingifier).map("tasks", params); + ThingReadRequestMapping mapping = map(thingifier, "tasks", params); Assertions.assertFalse(mapping.isError()); Assertions.assertEquals(1, mapping.getQuery().getQueryParams().size()); @@ -149,7 +147,13 @@ private Thingifier taskProjectThingifier() { } private ThingReadRequestMapper mapperFor(final Thingifier thingifier) { - return new ThingReadRequestMapper(thingifier); + return new ThingReadRequestMapper(new ThingifierSchemaCatalog(thingifier)); + } + + private ThingReadRequestMapping map( + final Thingifier thingifier, final String url, final QueryFilterParams queryParams) { + ThingifierSchemaCatalog schema = new ThingifierSchemaCatalog(thingifier); + return mapperFor(thingifier).map(new ThingRouteMapper(schema).map(url), queryParams); } private QueryFilterParams params() { diff --git a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingWriteRequestMapperTest.java b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingWriteRequestMapperTest.java index c5ba4b2f..2daa089d 100644 --- a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingWriteRequestMapperTest.java +++ b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingWriteRequestMapperTest.java @@ -1,20 +1,23 @@ package uk.co.compendiumdev.thingifier.api.restapihandlers; -import com.google.gson.Gson; import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; -import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingWriteRequestMapper; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingWriteRequestMapping; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingifierSchemaCatalog; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.ThingRoute; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.ThingRouteMapper; +import uk.co.compendiumdev.thingifier.api.http.bodyparser.ApiBodyFields; import uk.co.compendiumdev.thingifier.application.command.AmendThingCommand; -import uk.co.compendiumdev.thingifier.application.command.ConnectExistingRelationshipCommand; -import uk.co.compendiumdev.thingifier.application.command.CreateAndConnectRelationshipCommand; import uk.co.compendiumdev.thingifier.application.command.CreateThingCommand; import uk.co.compendiumdev.thingifier.application.command.DeleteThingCommand; import uk.co.compendiumdev.thingifier.application.command.DisconnectRelationshipCommand; +import uk.co.compendiumdev.thingifier.application.command.RelateThingCommand; +import uk.co.compendiumdev.thingifier.application.command.ReplaceThingCommand; import uk.co.compendiumdev.thingifier.core.EntityRelModel; import uk.co.compendiumdev.thingifier.core.domain.definitions.Cardinality; import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; @@ -30,7 +33,8 @@ public class ThingWriteRequestMapperTest { public void mapsPostCollectionToCreateCommand() { Thingifier thingifier = taskProjectThingifier(); ThingWriteRequestMapping mapping = - mapperFor(thingifier).mapPost("task", parserFor(thingifier, "title", "Task")); + mapperFor(thingifier) + .mapPost(routeFor(thingifier, "task"), parserFor("title", "Task")); Assertions.assertFalse(mapping.isError()); Assertions.assertTrue(mapping.getCommand() instanceof CreateThingCommand); @@ -44,8 +48,10 @@ public void mapsPostInstanceToPatchAmendCommand() { ThingWriteRequestMapping mapping = mapperFor(thingifier) .mapPost( - String.format("task/%s", task.getPrimaryKeyValue()), - parserFor(thingifier, "title", "Patched")); + routeFor( + thingifier, + String.format("task/%s", task.getPrimaryKeyValue())), + parserFor("title", "Patched")); Assertions.assertFalse(mapping.isError()); AmendThingCommand command = (AmendThingCommand) mapping.getCommand(); @@ -60,65 +66,43 @@ public void mapsPutExistingInstanceToReplaceAmendCommand() { ThingWriteRequestMapping mapping = mapperFor(thingifier) .mapPut( - String.format("task/%s", task.getPrimaryKeyValue()), - parserFor(thingifier, "title", "Replaced")); + routeFor( + thingifier, + String.format("task/%s", task.getPrimaryKeyValue())), + parserFor("title", "Replaced")); Assertions.assertFalse(mapping.isError()); - AmendThingCommand command = (AmendThingCommand) mapping.getCommand(); - Assertions.assertTrue(command.shouldReplaceExistingFieldsAndRelationships()); + Assertions.assertTrue(mapping.getCommand() instanceof ReplaceThingCommand); } @Test - public void mapsPutMissingStringKeyInstanceToCreateCommand() { + public void mapsPutInstanceToPutCommand() { Thingifier thingifier = stringKeyThingifier(); - ThingWriteRequestMapping mapping = - mapperFor(thingifier).mapPut("note/n-1", parserFor(thingifier, "title", "Created")); - - Assertions.assertFalse(mapping.isError()); - Assertions.assertTrue(mapping.getCommand() instanceof CreateThingCommand); - } - - @Test - public void rejectsPutCreateWhenBodyPrimaryKeyDoesNotMatchUrlKey() { - Thingifier thingifier = stringKeyThingifier(); - Map body = new HashMap<>(); - body.put("key", "other"); - body.put("title", "Created"); - - ThingWriteRequestMapping mapping = - mapperFor(thingifier).mapPut("note/n-1", parserFor(thingifier, body)); - - Assertions.assertTrue(mapping.isError()); - Assertions.assertEquals(400, mapping.getError().statusCode()); - } - - @Test - public void rejectsPutCreateWhenEntityHasAutoFields() { - Thingifier thingifier = taskProjectThingifier(); - ThingWriteRequestMapping mapping = mapperFor(thingifier) - .mapPut("task/manual", parserFor(thingifier, "title", "Created")); + .mapPut(routeFor(thingifier, "note/n-1"), parserFor("title", "Created")); - Assertions.assertTrue(mapping.isError()); - Assertions.assertEquals(400, mapping.getError().statusCode()); + Assertions.assertFalse(mapping.isError()); + Assertions.assertTrue(mapping.getCommand() instanceof ReplaceThingCommand); } @Test - public void postMissingInstanceMapsToNotFound() { + public void postInstanceMapsToUnresolvedAmendCommand() { Thingifier thingifier = taskProjectThingifier(); ThingWriteRequestMapping mapping = mapperFor(thingifier) - .mapPost("task/missing", parserFor(thingifier, "title", "Patched")); + .mapPost( + routeFor(thingifier, "task/missing"), + parserFor("title", "Patched")); - Assertions.assertTrue(mapping.isError()); - Assertions.assertEquals(404, mapping.getError().statusCode()); + Assertions.assertFalse(mapping.isError()); + Assertions.assertTrue(mapping.getCommand() instanceof AmendThingCommand); } @Test - public void mapsRelationshipPostWithExistingChildToConnectCommand() { + public void mapsRelationshipPostWithExistingChildToRelateCommand() { Thingifier thingifier = taskProjectThingifier(); EntityInstance task = createTask(thingifier, "Task"); EntityInstance project = createProject(thingifier, "Project"); @@ -126,26 +110,32 @@ public void mapsRelationshipPostWithExistingChildToConnectCommand() { ThingWriteRequestMapping mapping = mapperFor(thingifier) .mapPost( - String.format("project/%s/tasks", project.getPrimaryKeyValue()), - parserFor(thingifier, "guid", task.getPrimaryKeyValue())); + routeFor( + thingifier, + String.format( + "project/%s/tasks", project.getPrimaryKeyValue())), + parserFor("guid", task.getPrimaryKeyValue())); Assertions.assertFalse(mapping.isError()); - Assertions.assertTrue(mapping.getCommand() instanceof ConnectExistingRelationshipCommand); + Assertions.assertTrue(mapping.getCommand() instanceof RelateThingCommand); } @Test - public void mapsRelationshipPostWithoutChildKeyToCreateAndConnectCommand() { + public void mapsRelationshipPostWithoutChildKeyToRelateCommand() { Thingifier thingifier = taskProjectThingifier(); EntityInstance project = createProject(thingifier, "Project"); ThingWriteRequestMapping mapping = mapperFor(thingifier) .mapPost( - String.format("project/%s/tasks", project.getPrimaryKeyValue()), - parserFor(thingifier, "title", "New task")); + routeFor( + thingifier, + String.format( + "project/%s/tasks", project.getPrimaryKeyValue())), + parserFor("title", "New task")); Assertions.assertFalse(mapping.isError()); - Assertions.assertTrue(mapping.getCommand() instanceof CreateAndConnectRelationshipCommand); + Assertions.assertTrue(mapping.getCommand() instanceof RelateThingCommand); } @Test @@ -155,7 +145,10 @@ public void mapsDeleteInstanceToDeleteCommand() { ThingWriteRequestMapping mapping = mapperFor(thingifier) - .mapDelete(String.format("task/%s", task.getPrimaryKeyValue())); + .mapDelete( + routeFor( + thingifier, + String.format("task/%s", task.getPrimaryKeyValue()))); Assertions.assertFalse(mapping.isError()); Assertions.assertTrue(mapping.getCommand() instanceof DeleteThingCommand); @@ -172,9 +165,12 @@ public void mapsDeleteRelationshipPathToDisconnectCommand() { ThingWriteRequestMapping mapping = mapperFor(thingifier) .mapDelete( - String.format( - "project/%s/tasks/%s", - project.getPrimaryKeyValue(), task.getPrimaryKeyValue())); + routeFor( + thingifier, + String.format( + "project/%s/tasks/%s", + project.getPrimaryKeyValue(), + task.getPrimaryKeyValue()))); Assertions.assertFalse(mapping.isError()); Assertions.assertTrue(mapping.getCommand() instanceof DisconnectRelationshipCommand); @@ -186,7 +182,8 @@ public void mapsUnrecognisedPostToBadRequest() { ThingWriteRequestMapping mapping = mapperFor(thingifier) - .mapPost("not-understood", parserFor(thingifier, "title", "Nope")); + .mapPost( + routeFor(thingifier, "not-understood"), parserFor("title", "Nope")); Assertions.assertTrue(mapping.isError()); Assertions.assertEquals(400, mapping.getError().statusCode()); @@ -233,22 +230,24 @@ private EntityInstance createProject(final Thingifier thingifier, final String t } private ThingWriteRequestMapper mapperFor(final Thingifier thingifier) { - return new ThingWriteRequestMapper(thingifier, EntityRelModel.DEFAULT_DATABASE_NAME); + return new ThingWriteRequestMapper(new ThingifierSchemaCatalog(thingifier)); } private ThingStore storeFor(final Thingifier thingifier) { return thingifier.getStore(EntityRelModel.DEFAULT_DATABASE_NAME); } - private BodyParser parserFor( - final Thingifier thingifier, final String fieldName, final String fieldValue) { - Map body = new HashMap<>(); + private ThingRoute routeFor(final Thingifier thingifier, final String url) { + return new ThingRouteMapper(new ThingifierSchemaCatalog(thingifier)).map(url); + } + + private ApiBodyFields parserFor(final String fieldName, final String fieldValue) { + Map body = new HashMap<>(); body.put(fieldName, fieldValue); - return parserFor(thingifier, body); + return parserFor(body); } - private BodyParser parserFor(final Thingifier thingifier, final Map body) { - HttpApiRequest request = new HttpApiRequest("/path").setBody(new Gson().toJson(body)); - return new BodyParser(request, thingifier.getThingNames()); + private ApiBodyFields parserFor(final Map body) { + return ApiBodyFields.fromMap(body); } } diff --git a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/application/ThingCommandServiceTest.java b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/application/ThingCommandServiceTest.java index e263ed27..9f0660a5 100644 --- a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/application/ThingCommandServiceTest.java +++ b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/application/ThingCommandServiceTest.java @@ -4,18 +4,24 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import uk.co.compendiumdev.thingifier.Thingifier; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingifierSchemaCatalog; import uk.co.compendiumdev.thingifier.application.command.AmendThingCommand; +import uk.co.compendiumdev.thingifier.application.command.BodyFieldValue; import uk.co.compendiumdev.thingifier.application.command.ConnectExistingRelationshipCommand; import uk.co.compendiumdev.thingifier.application.command.CreateAndConnectRelationshipCommand; import uk.co.compendiumdev.thingifier.application.command.CreateThingCommand; import uk.co.compendiumdev.thingifier.application.command.DeleteThingCommand; import uk.co.compendiumdev.thingifier.application.command.DisconnectRelationshipCommand; +import uk.co.compendiumdev.thingifier.application.command.RelateThingCommand; import uk.co.compendiumdev.thingifier.application.command.RelationshipReference; +import uk.co.compendiumdev.thingifier.application.command.ReplaceThingCommand; +import uk.co.compendiumdev.thingifier.application.command.ThingWriteCommand; import uk.co.compendiumdev.thingifier.core.EntityRelModel; import uk.co.compendiumdev.thingifier.core.domain.definitions.Cardinality; import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field; import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.Optionality; import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipDefinition; import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; @@ -37,11 +43,11 @@ public void createCommandCreatesThingAndConnectsRelationships() { .withField("title", "Project")); ThingCommandResult result = - new ThingCommandService(store) + serviceFor(thingifier, store) .execute( new CreateThingCommand( - EntityInstanceDraft.forEntity(task) - .withField("title", "Task"), + task.getName(), + fields("title", "Task"), List.of( RelationshipReference.compressed( "task-of", @@ -69,15 +75,15 @@ public void createCommandCanResolveExplicitRelationshipReferenceByTargetField() .withField("title", "Project")); ThingCommandResult result = - new ThingCommandService(store) + serviceFor(thingifier, store) .execute( new CreateThingCommand( - EntityInstanceDraft.forEntity(task) - .withField("title", "Task"), + task.getName(), + fields("title", "Task"), List.of( RelationshipReference.explicit( "task-of", - project, + project.getName(), "projects", "title", "Project")), @@ -97,11 +103,11 @@ public void createCommandRollsBackWhenRelationshipReferenceCannotBeResolved() { int taskCount = store.entityQueries().count(task); ThingCommandResult result = - new ThingCommandService(store) + serviceFor(thingifier, store) .execute( new CreateThingCommand( - EntityInstanceDraft.forEntity(task) - .withField("title", "Task"), + task.getName(), + fields("title", "Task"), List.of( RelationshipReference.compressed( "task-of", "guid", "missing")), @@ -129,11 +135,11 @@ public void createCommandDoesNotDeleteExistingRelatedItemWhenFinalValidationFail int taskCount = store.entityQueries().count(task); ThingCommandResult result = - new ThingCommandService(store) + serviceFor(thingifier, store) .execute( new CreateThingCommand( - EntityInstanceDraft.forEntity(task) - .withField("title", "Task"), + task.getName(), + fields("title", "Task"), List.of( RelationshipReference.compressed( "task-of", @@ -148,6 +154,139 @@ public void createCommandDoesNotDeleteExistingRelatedItemWhenFinalValidationFail .findByQueryIdentifier(project, projectInstance.getPrimaryKeyValue())); } + @Test + public void createCommandRejectsDuplicateRequestedPrimaryKey() { + Thingifier thingifier = taskProjectThingifier(); + ThingStore store = storeFor(thingifier); + EntityDefinition project = thingifier.getDefinitionNamed("project"); + EntityInstance existing = + store.entities() + .create( + EntityInstanceDraft.forEntity(project) + .withField("title", "Existing")); + + ThingCommandResult result = + serviceFor(thingifier, store) + .execute( + new CreateThingCommand( + project.getName(), + existing.getPrimaryKeyValue(), + fields("title", "Duplicate"), + List.of(), + true)); + + Assertions.assertTrue(result.isError()); + Assertions.assertEquals(ApplicationError.Category.CONFLICT, result.getError().category()); + Assertions.assertEquals(ApplicationError.Code.CONFLICT, result.getError().code()); + Assertions.assertEquals(1, store.entityQueries().count(project)); + } + + @Test + public void replaceCreateRejectsAutoFieldDefinitions() { + Thingifier thingifier = taskProjectThingifier(); + ThingStore store = storeFor(thingifier); + + ThingCommandResult result = + serviceFor(thingifier, store) + .execute( + new ReplaceThingCommand( + "task", "123", fields("title", "Nope"), List.of())); + + Assertions.assertTrue(result.isError()); + Assertions.assertEquals( + ApplicationError.Code.REPLACE_CREATE_AUTO_FIELDS_NOT_ALLOWED, + result.getError().code()); + Assertions.assertEquals("task", result.getError().detail("entityName")); + Assertions.assertEquals("guid", result.getError().detail("fieldNames")); + Assertions.assertFalse(result.getCombinedErrorMessage().contains("PUT")); + } + + @Test + public void replaceCreateRejectsMismatchedIdentityField() { + Thingifier thingifier = thingifierWithStringPrimaryKey(); + ThingStore store = storeFor(thingifier); + + ThingCommandResult result = + serviceFor(thingifier, store) + .execute( + new ReplaceThingCommand( + "task", "route-key", fields("ref", "body-key"), List.of())); + + Assertions.assertTrue(result.isError()); + Assertions.assertEquals( + ApplicationError.Code.REPLACE_CREATE_KEY_MISMATCH, result.getError().code()); + Assertions.assertEquals("task", result.getError().detail("entityName")); + Assertions.assertEquals("route-key", result.getError().detail("routeIdentifier")); + Assertions.assertEquals("body-key", result.getError().detail("bodyIdentifier")); + Assertions.assertFalse(result.getCombinedErrorMessage().contains("PUT")); + } + + @Test + public void unsupportedCommandReturnsSemanticErrorCode() { + Thingifier thingifier = taskProjectThingifier(); + ThingStore store = storeFor(thingifier); + + ThingCommandResult result = + serviceFor(thingifier, store).execute(new UnsupportedThingWriteCommand()); + + Assertions.assertTrue(result.isError()); + Assertions.assertEquals( + ApplicationError.Category.UNSUPPORTED, result.getError().category()); + Assertions.assertEquals( + ApplicationError.Code.UNSUPPORTED_COMMAND, result.getError().code()); + } + + @Test + public void createCommandValidatesDeclaredBodyTypesInApplication() { + Thingifier thingifier = typedTodoThingifier(); + ThingStore store = storeFor(thingifier); + + ThingCommandResult result = + serviceFor(thingifier, store, true) + .execute( + new CreateThingCommand( + "todo", + "", + fields("doneStatus", "true"), + List.of( + bodyField( + "doneStatus", + "true", + BodyFieldValue.SourceType.STRING)), + List.of(), + true)); + + Assertions.assertTrue(result.isError()); + Assertions.assertEquals( + List.of("doneStatus should be BOOLEAN but was STRING"), result.getErrorMessages()); + Assertions.assertEquals( + 0, store.entityQueries().count(thingifier.getDefinitionNamed("todo"))); + } + + @Test + public void createCommandNormalizesNumericIntegerBodyValuesInApplication() { + Thingifier thingifier = typedTodoThingifier(); + ThingStore store = storeFor(thingifier); + + ThingCommandResult result = + serviceFor(thingifier, store, true) + .execute( + new CreateThingCommand( + "todo", + "", + fields("priority", "2.0"), + List.of( + bodyField( + "priority", + "2.0", + BodyFieldValue.SourceType.NUMERIC)), + List.of(), + true)); + + Assertions.assertTrue(result.isSuccessful()); + Assertions.assertEquals("2", result.getInstance().getFieldValue("priority").asString()); + } + @Test public void amendCommandRollsBackFieldsAndRelationshipsOnValidationFailure() { Thingifier thingifier = mandatoryTaskProjectThingifier(); @@ -167,12 +306,12 @@ public void amendCommandRollsBackFieldsAndRelationshipsOnValidationFailure() { store.relationships().connect(taskInstance, "task-of", projectInstance); ThingCommandResult result = - new ThingCommandService(store) + serviceFor(thingifier, store) .execute( new AmendThingCommand( - taskInstance, - EntityInstanceDraft.forEntity(task) - .withField("title", "Rejected title"), + task.getName(), + taskInstance.getPrimaryKeyValue(), + fields("title", "Rejected title"), true, List.of())); @@ -204,12 +343,12 @@ public void amendCommandRollsBackWhenRelationshipReferenceCannotBeResolved() { store.relationships().connect(taskInstance, "task-of", projectInstance); ThingCommandResult result = - new ThingCommandService(store) + serviceFor(thingifier, store) .execute( new AmendThingCommand( - taskInstance, - EntityInstanceDraft.forEntity(task) - .withField("title", "Rejected title"), + task.getName(), + taskInstance.getPrimaryKeyValue(), + fields("title", "Rejected title"), false, List.of( RelationshipReference.compressed( @@ -239,7 +378,10 @@ public void deleteCommandDeletesInstance() { .withField("title", "Delete me")); ThingCommandResult result = - new ThingCommandService(store).execute(new DeleteThingCommand(taskInstance)); + serviceFor(thingifier, store) + .execute( + new DeleteThingCommand( + task.getName(), taskInstance.getPrimaryKeyValue())); Assertions.assertTrue(result.isSuccessful()); Assertions.assertNull( @@ -247,6 +389,22 @@ public void deleteCommandDeletesInstance() { .findByQueryIdentifier(task, taskInstance.getPrimaryKeyValue())); } + @Test + public void deleteMissingInstanceReturnsSemanticNotFoundDetails() { + Thingifier thingifier = taskProjectThingifier(); + ThingStore store = storeFor(thingifier); + + ThingCommandResult result = + serviceFor(thingifier, store).execute(new DeleteThingCommand("task", "missing")); + + Assertions.assertTrue(result.isError()); + Assertions.assertEquals(ApplicationError.Code.INSTANCE_NOT_FOUND, result.getError().code()); + Assertions.assertEquals("task", result.getError().detail("entityName")); + Assertions.assertEquals("missing", result.getError().detail("identifier")); + Assertions.assertFalse( + result.getCombinedErrorMessage().contains("Could not find any instances with")); + } + @Test public void disconnectRelationshipCommandRemovesOnlyRelationship() { Thingifier thingifier = taskProjectThingifier(); @@ -264,10 +422,13 @@ public void disconnectRelationshipCommandRemovesOnlyRelationship() { store.relationships().connect(projectInstance, "tasks", taskInstance); ThingCommandResult result = - new ThingCommandService(store) + serviceFor(thingifier, store) .execute( new DisconnectRelationshipCommand( - projectInstance, taskInstance, "tasks")); + project.getName(), + projectInstance.getPrimaryKeyValue(), + "tasks", + taskInstance.getPrimaryKeyValue())); Assertions.assertTrue(result.isSuccessful()); Assertions.assertTrue( @@ -277,6 +438,50 @@ public void disconnectRelationshipCommandRemovesOnlyRelationship() { .findByQueryIdentifier(task, taskInstance.getPrimaryKeyValue())); } + @Test + public void disconnectRelationshipMissingSourceReturnsSemanticNotFoundDetails() { + Thingifier thingifier = taskProjectThingifier(); + ThingStore store = storeFor(thingifier); + + ThingCommandResult result = + serviceFor(thingifier, store) + .execute( + new DisconnectRelationshipCommand( + "project", "missing-project", "tasks", "missing-task")); + + Assertions.assertTrue(result.isError()); + Assertions.assertEquals( + ApplicationError.Code.RELATIONSHIP_SOURCE_NOT_FOUND, result.getError().code()); + Assertions.assertEquals("project", result.getError().detail("entityName")); + Assertions.assertEquals("missing-project", result.getError().detail("identifier")); + Assertions.assertEquals("tasks", result.getError().detail("relationshipName")); + } + + @Test + public void disconnectRelationshipMissingTargetReturnsSemanticNotFoundDetails() { + Thingifier thingifier = taskProjectThingifier(); + ThingStore store = storeFor(thingifier); + EntityInstance project = createProject(thingifier, "Project"); + + ThingCommandResult result = + serviceFor(thingifier, store) + .execute( + new DisconnectRelationshipCommand( + "project", + project.getPrimaryKeyValue(), + "tasks", + "missing-task")); + + Assertions.assertTrue(result.isError()); + Assertions.assertEquals( + ApplicationError.Code.RELATIONSHIP_TARGET_NOT_FOUND, result.getError().code()); + Assertions.assertEquals("project", result.getError().detail("entityName")); + Assertions.assertEquals( + project.getPrimaryKeyValue(), result.getError().detail("identifier")); + Assertions.assertEquals("tasks", result.getError().detail("relationshipName")); + Assertions.assertEquals("missing-task", result.getError().detail("childIdentifier")); + } + @Test public void failedConnectExistingRelationshipDoesNotDeleteExistingChild() { Thingifier thingifier = taskProjectThingifierWithMandatoryCategory(); @@ -295,10 +500,13 @@ public void failedConnectExistingRelationshipDoesNotDeleteExistingChild() { .withField("title", "Project")); ThingCommandResult result = - new ThingCommandService(store) + serviceFor(thingifier, store) .execute( new ConnectExistingRelationshipCommand( - projectInstance, "tasks", taskInstance)); + project.getName(), + projectInstance.getPrimaryKeyValue(), + "tasks", + fields("guid", taskInstance.getPrimaryKeyValue()))); Assertions.assertTrue(result.isError()); Assertions.assertNotNull( @@ -322,13 +530,14 @@ public void failedCreateAndConnectRelationshipRollsBackCreatedChild() { int taskCount = store.entityQueries().count(task); ThingCommandResult result = - new ThingCommandService(store) + serviceFor(thingifier, store) .execute( new CreateAndConnectRelationshipCommand( - projectInstance, + project.getName(), + projectInstance.getPrimaryKeyValue(), "tasks", - EntityInstanceDraft.forEntity(task) - .withField("title", "Rolled back"), + task.getName(), + fields("title", "Rolled back"), List.of())); Assertions.assertTrue(result.isError()); @@ -352,13 +561,14 @@ public void failedCreateAndConnectRelationshipRollsBackOnUnresolvedChildRelation int taskCount = store.entityQueries().count(task); ThingCommandResult result = - new ThingCommandService(store) + serviceFor(thingifier, store) .execute( new CreateAndConnectRelationshipCommand( - projectInstance, + project.getName(), + projectInstance.getPrimaryKeyValue(), "tasks", - EntityInstanceDraft.forEntity(task) - .withField("title", "Rolled back"), + task.getName(), + fields("title", "Rolled back"), List.of( RelationshipReference.compressed( "category", "guid", "missing")))); @@ -369,6 +579,128 @@ public void failedCreateAndConnectRelationshipRollsBackOnUnresolvedChildRelation store.relationships().listRelated(projectInstance, "tasks").isEmpty()); } + @Test + public void relateCommandConnectsExistingTargetWithoutCreatingChild() { + Thingifier thingifier = taskProjectThingifier(); + ThingStore store = storeFor(thingifier); + EntityDefinition task = thingifier.getDefinitionNamed("task"); + EntityDefinition project = thingifier.getDefinitionNamed("project"); + EntityInstance taskInstance = + store.entities() + .create(EntityInstanceDraft.forEntity(task).withField("title", "Task")); + EntityInstance projectInstance = + store.entities() + .create( + EntityInstanceDraft.forEntity(project) + .withField("title", "Project")); + + ThingCommandResult result = + serviceFor(thingifier, store) + .execute( + new RelateThingCommand( + project.getName(), + projectInstance.getPrimaryKeyValue(), + "tasks", + fields("guid", taskInstance.getPrimaryKeyValue()), + List.of())); + + Assertions.assertTrue(result.isSuccessful()); + Assertions.assertFalse(result.createdInstance()); + Assertions.assertEquals(1, store.entityQueries().count(task)); + Assertions.assertEquals( + taskInstance, store.relationships().listRelated(projectInstance, "tasks").get(0)); + } + + @Test + public void relateMissingParentReturnsSemanticNotFoundDetails() { + Thingifier thingifier = taskProjectThingifier(); + ThingStore store = storeFor(thingifier); + + ThingCommandResult result = + serviceFor(thingifier, store) + .execute( + new RelateThingCommand( + "project", + "missing-project", + "tasks", + fields("title", "Task"), + List.of())); + + Assertions.assertTrue(result.isError()); + Assertions.assertEquals( + ApplicationError.Code.PARENT_INSTANCE_NOT_FOUND, result.getError().code()); + Assertions.assertEquals("project", result.getError().detail("entityName")); + Assertions.assertEquals("missing-project", result.getError().detail("identifier")); + Assertions.assertEquals("tasks", result.getError().detail("relationshipName")); + } + + @Test + public void relateCommandAllowsStringAutoIncrementReferenceWhenConnectingExistingTarget() { + Thingifier thingifier = numericIdTaskProjectThingifier(); + ThingStore store = storeFor(thingifier); + EntityDefinition task = thingifier.getDefinitionNamed("task"); + EntityDefinition project = thingifier.getDefinitionNamed("project"); + EntityInstance taskInstance = + store.entities() + .create(EntityInstanceDraft.forEntity(task).withField("title", "Task")); + EntityInstance projectInstance = + store.entities() + .create( + EntityInstanceDraft.forEntity(project) + .withField("title", "Project")); + + ThingCommandResult result = + serviceFor(thingifier, store, true) + .execute( + new RelateThingCommand( + project.getName(), + projectInstance.getPrimaryKeyValue(), + "tasks", + fields("id", taskInstance.getPrimaryKeyValue()), + List.of( + bodyField( + "id", + taskInstance.getPrimaryKeyValue(), + BodyFieldValue.SourceType.STRING)), + List.of())); + + Assertions.assertTrue(result.isSuccessful()); + Assertions.assertFalse(result.createdInstance()); + Assertions.assertEquals(1, store.entityQueries().count(task)); + Assertions.assertEquals( + taskInstance, store.relationships().listRelated(projectInstance, "tasks").get(0)); + } + + @Test + public void relateCommandCreatesAndConnectsWhenNoTargetReferenceIsSupplied() { + Thingifier thingifier = taskProjectThingifier(); + ThingStore store = storeFor(thingifier); + EntityDefinition task = thingifier.getDefinitionNamed("task"); + EntityDefinition project = thingifier.getDefinitionNamed("project"); + EntityInstance projectInstance = + store.entities() + .create( + EntityInstanceDraft.forEntity(project) + .withField("title", "Project")); + + ThingCommandResult result = + serviceFor(thingifier, store) + .execute( + new RelateThingCommand( + project.getName(), + projectInstance.getPrimaryKeyValue(), + "tasks", + fields("title", "Created task"), + List.of())); + + Assertions.assertTrue(result.isSuccessful()); + Assertions.assertTrue(result.createdInstance()); + Assertions.assertEquals(1, store.entityQueries().count(task)); + Assertions.assertEquals( + result.getInstance(), + store.relationships().listRelated(projectInstance, "tasks").get(0)); + } + private Thingifier taskProjectThingifier() { Thingifier thingifier = new Thingifier(); EntityDefinition task = thingifier.defineThing("task", "tasks"); @@ -415,7 +747,67 @@ private Thingifier taskProjectThingifierWithMandatoryCategory() { return thingifier; } + private Thingifier typedTodoThingifier() { + Thingifier thingifier = new Thingifier(); + EntityDefinition todo = thingifier.defineThing("todo", "todos"); + todo.addField(Field.is("doneStatus", FieldType.BOOLEAN)); + todo.addField(Field.is("priority", FieldType.INTEGER)); + return thingifier; + } + + private Thingifier thingifierWithStringPrimaryKey() { + Thingifier thingifier = new Thingifier(); + EntityDefinition task = thingifier.defineThing("task", "tasks"); + task.addAsPrimaryKeyField(Field.is("ref", FieldType.STRING)); + task.addField(Field.is("title", FieldType.STRING)); + return thingifier; + } + + private Thingifier numericIdTaskProjectThingifier() { + Thingifier thingifier = new Thingifier(); + EntityDefinition task = thingifier.defineThing("task", "tasks"); + task.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT)); + task.addField(Field.is("title", FieldType.STRING)); + + EntityDefinition project = thingifier.defineThing("project", "projects"); + project.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT)); + project.addField(Field.is("title", FieldType.STRING)); + + thingifier + .defineRelationship(project, task, "tasks", Cardinality.ONE_TO_MANY()) + .whenReversed(Cardinality.ONE_TO_MANY(), "tasksof"); + return thingifier; + } + private ThingStore storeFor(final Thingifier thingifier) { return thingifier.getStore(EntityRelModel.DEFAULT_DATABASE_NAME); } + + private EntityInstance createProject(final Thingifier thingifier, final String title) { + EntityDefinition project = thingifier.getDefinitionNamed("project"); + return storeFor(thingifier) + .entities() + .create(EntityInstanceDraft.forEntity(project).withField("title", title)); + } + + private ThingCommandService serviceFor(final Thingifier thingifier, final ThingStore store) { + return new ThingCommandService(store, new ThingifierSchemaCatalog(thingifier)); + } + + private ThingCommandService serviceFor( + final Thingifier thingifier, final ThingStore store, final boolean enforceTypes) { + return new ThingCommandService( + store, new ThingifierSchemaCatalog(thingifier), enforceTypes); + } + + private List fields(final String name, final String value) { + return List.of(new NamedValue(name, value)); + } + + private BodyFieldValue bodyField( + final String name, final String value, final BodyFieldValue.SourceType sourceType) { + return new BodyFieldValue(name, value, sourceType); + } + + private static final class UnsupportedThingWriteCommand implements ThingWriteCommand {} } diff --git a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/application/ThingQueryServiceTest.java b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/application/ThingQueryServiceTest.java index e011cef9..093ccd21 100644 --- a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/application/ThingQueryServiceTest.java +++ b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/application/ThingQueryServiceTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import uk.co.compendiumdev.thingifier.Thingifier; +import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.ThingifierSchemaCatalog; import uk.co.compendiumdev.thingifier.application.query.ReadCollectionQuery; import uk.co.compendiumdev.thingifier.application.query.ReadInstanceQuery; import uk.co.compendiumdev.thingifier.application.query.ReadRelationshipQuery; @@ -29,9 +30,9 @@ public void collectionReadReturnsAllInstances() { EntityInstance second = createTask(storeFor(thingifier), task, "Second", "1"); RepositoryQueryResult result = - new ThingQueryService() + queryServiceFor(thingifier) .execute( - new ReadCollectionQuery(task, new QueryFilterParams()), + new ReadCollectionQuery(task.getName(), new QueryFilterParams()), storeFor(thingifier)); Assertions.assertTrue(result.isResultACollection()); @@ -47,10 +48,10 @@ public void instanceReadReturnsMatchingInstance() { EntityInstance instance = createTask(storeFor(thingifier), task, "Only", "1"); RepositoryQueryResult result = - new ThingQueryService() + queryServiceFor(thingifier) .execute( new ReadInstanceQuery( - task, + task.getName(), instance.getPrimaryKeyValue(), new QueryFilterParams()), storeFor(thingifier)); @@ -66,9 +67,10 @@ public void missingInstanceReadReportsNoMatch() { EntityDefinition task = thingifier.getDefinitionNamed("task"); RepositoryQueryResult result = - new ThingQueryService() + queryServiceFor(thingifier) .execute( - new ReadInstanceQuery(task, "missing", new QueryFilterParams()), + new ReadInstanceQuery( + task.getName(), "missing", new QueryFilterParams()), storeFor(thingifier)); Assertions.assertTrue(result.wasQueryIntendedToMatchAnInstance()); @@ -91,10 +93,10 @@ public void relationshipReadReturnsRelatedInstances() { store.relationships().connect(projectInstance, "tasks", taskInstance); RepositoryQueryResult result = - new ThingQueryService() + queryServiceFor(thingifier) .execute( new ReadRelationshipQuery( - project, + project.getName(), projectInstance.getPrimaryKeyValue(), "tasks", new QueryFilterParams()), @@ -111,10 +113,13 @@ public void missingRelationshipSourceReportsNoMatch() { EntityDefinition project = thingifier.getDefinitionNamed("project"); RepositoryQueryResult result = - new ThingQueryService() + queryServiceFor(thingifier) .execute( new ReadRelationshipQuery( - project, "missing", "tasks", new QueryFilterParams()), + project.getName(), + "missing", + "tasks", + new QueryFilterParams()), storeFor(thingifier)); Assertions.assertTrue(result.lastMatchWasNothing()); @@ -131,8 +136,10 @@ public void collectionReadAppliesFilters() { params.put("title", "=Keep"); RepositoryQueryResult result = - new ThingQueryService() - .execute(new ReadCollectionQuery(task, params), storeFor(thingifier)); + queryServiceFor(thingifier) + .execute( + new ReadCollectionQuery(task.getName(), params), + storeFor(thingifier)); Assertions.assertEquals(1, result.getListEntityInstances().size()); Assertions.assertEquals( @@ -149,8 +156,10 @@ public void collectionReadAppliesSorting() { params.put("sortBy", "+priority"); RepositoryQueryResult result = - new ThingQueryService() - .execute(new ReadCollectionQuery(task, params), storeFor(thingifier)); + queryServiceFor(thingifier) + .execute( + new ReadCollectionQuery(task.getName(), params), + storeFor(thingifier)); Assertions.assertEquals( "First", result.getListEntityInstances().get(0).getFieldValue("title").asString()); @@ -168,8 +177,11 @@ public void sqliteBackedStoreExecutesReadQuery() { EntityInstance instance = createTask(store, task, "SQLite", "1"); RepositoryQueryResult result = - new ThingQueryService() - .execute(new ReadCollectionQuery(task, new QueryFilterParams()), store); + queryServiceFor(thingifier) + .execute( + new ReadCollectionQuery( + task.getName(), new QueryFilterParams()), + store); Assertions.assertEquals(List.of(instance), result.getListEntityInstances()); } @@ -207,4 +219,8 @@ private EntityInstance createTask( private ThingStore storeFor(final Thingifier thingifier) { return thingifier.getStore(EntityRelModel.DEFAULT_DATABASE_NAME); } + + private ThingQueryService queryServiceFor(final Thingifier thingifier) { + return new ThingQueryService(new ThingifierSchemaCatalog(thingifier)); + } } diff --git a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/architecture/ArchitectureBoundaryTest.java b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/architecture/ArchitectureBoundaryTest.java index a32b0875..2f4447e5 100644 --- a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/architecture/ArchitectureBoundaryTest.java +++ b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/architecture/ArchitectureBoundaryTest.java @@ -46,28 +46,6 @@ public void commandAndQueryPackagesDoNotImportApiResponse() throws IOException { Assertions.assertFalse(querySources.contains("ApiResponse")); } - @Test - public void relationshipBodyParserDoesNotResolveRepositoryInstances() throws IOException { - String parserSource = - Files.readString( - moduleRoot() - .resolve( - "src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipBodyCommandParser.java")); - - Assertions.assertFalse(parserSource.contains("entityQueries()")); - Assertions.assertFalse(parserSource.contains("findByField(")); - Assertions.assertFalse(parserSource.contains("findByQueryIdentifier(")); - } - - @Test - public void coreDoesNotContainUrlRouteQueryParser() { - Assertions.assertFalse( - Files.exists( - coreMainRoot() - .resolve( - "uk/co/compendiumdev/thingifier/core/query/RepositoryUrlQuery.java"))); - } - @Test public void relationshipRepositoryRemovalApiDoesNotExposeCascadeDecisions() throws NoSuchMethodException { @@ -105,13 +83,4 @@ private Path moduleRoot() { } return cwd.resolve("thingifier"); } - - private Path coreMainRoot() { - Path cwd = Paths.get("").toAbsolutePath(); - Path sibling = cwd.resolve("../ercoremodel/src/main/java").normalize(); - if (Files.exists(sibling)) { - return sibling; - } - return cwd.resolve("ercoremodel/src/main/java"); - } } diff --git a/thingifierapp/src/main/java/uk/co/compendiumdev/thingifier/application/Main.java b/thingifierapp/src/main/java/uk/co/compendiumdev/thingifier/application/Main.java index 81ed19f8..c9376e8a 100644 --- a/thingifierapp/src/main/java/uk/co/compendiumdev/thingifier/application/Main.java +++ b/thingifierapp/src/main/java/uk/co/compendiumdev/thingifier/application/Main.java @@ -1,8 +1,8 @@ package uk.co.compendiumdev.thingifier.application; import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.http.ThingifierHttpApiRoutings; import uk.co.compendiumdev.thingifier.adapter.spark.MainImplementation; +import uk.co.compendiumdev.thingifier.adapter.spark.ThingifierHttpApiRoutings; import uk.co.compendiumdev.thingifier.application.examples.TodoListThingifier; import uk.co.compendiumdev.thingifier.application.examples.TodoManagerThingifier; diff --git a/todoManagerRestAuto/src/test/java/uk/co/compendiumdev/thingifier/tactical/postmanreplication/Environment.java b/todoManagerRestAuto/src/test/java/uk/co/compendiumdev/thingifier/tactical/postmanreplication/Environment.java index 64ed1b6d..97817c7c 100644 --- a/todoManagerRestAuto/src/test/java/uk/co/compendiumdev/thingifier/tactical/postmanreplication/Environment.java +++ b/todoManagerRestAuto/src/test/java/uk/co/compendiumdev/thingifier/tactical/postmanreplication/Environment.java @@ -6,8 +6,8 @@ import spark.Spark; import uk.co.compendiumdev.sparkstart.Port; import uk.co.compendiumdev.thingifier.Thingifier; -import uk.co.compendiumdev.thingifier.adapter.http.ThingifierAutoDocGenRouting; -import uk.co.compendiumdev.thingifier.adapter.http.ThingifierHttpApiRoutings; +import uk.co.compendiumdev.thingifier.adapter.spark.ThingifierAutoDocGenRouting; +import uk.co.compendiumdev.thingifier.adapter.spark.ThingifierHttpApiRoutings; import uk.co.compendiumdev.thingifier.api.docgen.ThingifierApiDocumentationDefn; import uk.co.compendiumdev.thingifier.application.examples.TodoManagerThingifier; import uk.co.compendiumdev.thingifier.htmlgui.htmlgen.DefaultGUIHTML; @@ -33,7 +33,6 @@ public static String getBaseUri() { } else { // start it up Spark.port(4567); - String[] args = {}; final Thingifier thingifier = new TodoManagerThingifier().get(); thingifier.apiConfig().adminConfig().enableAdminDataClear(); thingifier.apiConfig().adminConfig().enableAdminSearch();