From fa0160515294a50f431208b078ba41dceffd0f12 Mon Sep 17 00:00:00 2001 From: Alan Richardson Date: Tue, 14 Jul 2026 10:44:00 +0100 Subject: [PATCH 1/6] Refine outside-in API boundaries --- .../challenge/ChallengeMain.java | 2 +- .../challenge/ChallengeRouteHandler.java | 2 +- .../challengesrouting/AuthRoutes.java | 6 +- .../ChallengerTrackingRoutes.java | 2 +- .../challengesrouting/ChallengesRoutes.java | 4 +- .../challengesrouting/HeartBeatRoutes.java | 2 +- .../practicemodes/mirror/MirrorRoutes.java | 4 +- .../practicemodes/mirror/RequestMirror.java | 2 +- .../simpleapi/SimpleApiRoutes.java | 6 +- .../simulation/SimulationRoutes.java | 6 +- .../spark/SparkMessageLengthValidator.java | 4 +- ...030GetTodosNotAcceptableHeader406Test.java | 18 +- .../C035PutRestoreChallengerSessionTest.java | 22 +- .../C038PutRestoreChallengerTodosTest.java | 19 +- checkstyle-project-fqn.xml | 64 +++ .../todolist/application/Main.java | 4 +- .../bootstrap/ThingifierServerBootstrap.java | 25 ++ .../http/apihandlers}/ApiMappingError.java | 2 +- .../apihandlers}/BodyCreationValidator.java | 14 +- .../DefaultThingifierApiRuntime.java | 52 +++ .../http/apihandlers}/FieldValues.java | 2 +- .../RelationshipBodyCommandParser.java | 15 +- .../RelationshipBodyCommands.java | 2 +- .../http/apihandlers}/SchemaCatalog.java | 2 +- .../apihandlers}/ThingBodyCommandMapper.java | 148 ++++--- .../ThingCommandResultApiMapper.java | 16 +- .../apihandlers/ThingReadRequestMapper.java | 63 +++ .../apihandlers}/ThingReadRequestMapping.java | 2 +- .../ThingReadResultApiMapper.java | 2 +- .../apihandlers/ThingWriteRequestMapper.java | 180 ++++++++ .../ThingWriteRequestMapping.java | 2 +- .../apihandlers/ThingifierApiRuntime.java | 23 ++ .../apihandlers}/ThingifierSchemaCatalog.java | 6 +- .../commonerrorresponse/NoSuchEntity.java | 4 +- .../apihandlers/route/CollectionRoute.java | 17 + .../http/apihandlers/route/InstanceRoute.java | 24 ++ .../route/RelationshipCollectionRoute.java | 33 ++ .../route/RelationshipInstanceRoute.java | 40 ++ .../http/apihandlers/route/ThingRoute.java | 14 + .../apihandlers/route/ThingRouteMapper.java | 57 +++ .../apihandlers/route/UnmatchedRoute.java | 26 ++ .../AdhocDocumentedSparkRouteConfigurer.java | 1 - .../adapter/spark/MainImplementation.java | 17 +- .../spark/SimpleSparkRouteCreator.java | 2 +- .../{ => adapter}/spark/SparkAsText.java | 2 +- .../SparkHttpGenericExceptionRoutings.java | 3 +- .../ThingifierAutoDocGenRouting.java | 2 +- .../ThingifierHttpApiRoutings.java | 8 +- .../InternalHttpResponseToSpark.java | 2 +- .../SparkResponseToInternalHttpResponse.java | 2 +- .../SparkToInternalHttpRequest.java | 2 +- .../routehandlers/ShutdownRouteHandler.java | 2 +- .../SparkApiRequestResponseHandler.java | 7 +- .../api/ThingifierRestAPIHandler.java | 67 ++- .../api/http/ApiRequestEnvelope.java | 65 +++ .../api/http/ThingifierHttpApi.java | 37 +- .../api/http/bodyparser/ApiBodyField.java | 20 + .../api/http/bodyparser/ApiBodyFields.java | 168 ++++++++ .../api/http/bodyparser/BodyParser.java | 4 + .../api/restapihandlers/EntityUrlMatcher.java | 70 ---- ...positoryBackedRelationshipUrlResolver.java | 156 ------- .../restapihandlers/RestApiDeleteHandler.java | 25 +- .../restapihandlers/RestApiGetHandler.java | 33 +- .../restapihandlers/RestApiPostHandler.java | 33 +- .../restapihandlers/RestApiPutHandler.java | 33 +- .../ThingReadRequestMapper.java | 65 --- .../ThingWriteRequestMapper.java | 312 -------------- .../application/ApplicationError.java | 59 +++ .../EntityInstanceDraftBuilder.java | 106 +++++ .../application/ThingCommandResult.java | 44 +- .../application/ThingCommandService.java | 385 +++++++++++++++++- .../command/AmendThingCommand.java | 62 +++ .../ConnectExistingRelationshipCommand.java | 49 +++ .../CreateAndConnectRelationshipCommand.java | 39 ++ .../command/DeleteThingCommand.java | 31 ++ .../DisconnectRelationshipCommand.java | 44 ++ .../application/command/PutThingCommand.java | 42 ++ .../route/ThingRouteMapperTest.java | 83 ++++ .../api/http/ApiRequestEnvelopeTest.java | 47 +++ .../RelationshipBodyCommandParserTest.java | 36 +- ...toryBackedRelationshipUrlResolverTest.java | 179 -------- .../ThingReadRequestMapperTest.java | 37 +- .../ThingWriteRequestMapperTest.java | 123 +++--- .../ArchitectureBoundaryTest.java | 31 -- .../thingifier/application/Main.java | 2 +- .../postmanreplication/Environment.java | 5 +- 86 files changed, 2297 insertions(+), 1183 deletions(-) create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/bootstrap/ThingifierServerBootstrap.java rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/{api/restapihandlers => adapter/http/apihandlers}/ApiMappingError.java (94%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/{api/restapihandlers => adapter/http/apihandlers}/BodyCreationValidator.java (83%) create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/DefaultThingifierApiRuntime.java rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/{api/restapihandlers => adapter/http/apihandlers}/FieldValues.java (89%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/{api/restapihandlers => adapter/http/apihandlers}/RelationshipBodyCommandParser.java (91%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/{api/restapihandlers => adapter/http/apihandlers}/RelationshipBodyCommands.java (94%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/{api/restapihandlers => adapter/http/apihandlers}/SchemaCatalog.java (78%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/{api/restapihandlers => adapter/http/apihandlers}/ThingBodyCommandMapper.java (64%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/{api/restapihandlers => adapter/http/apihandlers}/ThingCommandResultApiMapper.java (82%) create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingReadRequestMapper.java rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/{api/restapihandlers => adapter/http/apihandlers}/ThingReadRequestMapping.java (92%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/{api/restapihandlers => adapter/http/apihandlers}/ThingReadResultApiMapper.java (96%) create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingWriteRequestMapper.java rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/{api/restapihandlers => adapter/http/apihandlers}/ThingWriteRequestMapping.java (93%) create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingifierApiRuntime.java rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/{api/restapihandlers => adapter/http/apihandlers}/ThingifierSchemaCatalog.java (74%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/{api/restapihandlers => adapter/http/apihandlers}/commonerrorresponse/NoSuchEntity.java (58%) create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/CollectionRoute.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/InstanceRoute.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/RelationshipCollectionRoute.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/RelationshipInstanceRoute.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/ThingRoute.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/ThingRouteMapper.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/UnmatchedRoute.java rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/{ => adapter}/spark/SimpleSparkRouteCreator.java (98%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/{ => adapter}/spark/SparkAsText.java (96%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/{http => spark}/SparkHttpGenericExceptionRoutings.java (92%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/{http => spark}/ThingifierAutoDocGenRouting.java (98%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/{http => spark}/ThingifierHttpApiRoutings.java (97%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/{internalhttp => spark}/conversion/InternalHttpResponseToSpark.java (93%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/{internalhttp => spark}/conversion/SparkResponseToInternalHttpResponse.java (92%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/{internalhttp => spark}/conversion/SparkToInternalHttpRequest.java (96%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/{http => spark}/routehandlers/ShutdownRouteHandler.java (95%) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/{http => spark}/routehandlers/SparkApiRequestResponseHandler.java (89%) create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/ApiRequestEnvelope.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/bodyparser/ApiBodyField.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/bodyparser/ApiBodyFields.java delete mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/EntityUrlMatcher.java delete mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RepositoryBackedRelationshipUrlResolver.java delete mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingReadRequestMapper.java delete mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingWriteRequestMapper.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ApplicationError.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/EntityInstanceDraftBuilder.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/PutThingCommand.java create mode 100644 thingifier/src/test/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/ThingRouteMapperTest.java create mode 100644 thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/http/ApiRequestEnvelopeTest.java delete mode 100644 thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RepositoryBackedRelationshipUrlResolverTest.java 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..f72ffbff 100644 --- a/checkstyle-project-fqn.xml +++ b/checkstyle-project-fqn.xml @@ -68,7 +68,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/api/restapihandlers/BodyCreationValidator.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/BodyCreationValidator.java similarity index 83% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyCreationValidator.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/BodyCreationValidator.java index 94476498..61b43b8c 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyCreationValidator.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/BodyCreationValidator.java @@ -1,8 +1,8 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; import java.util.List; import java.util.Map; -import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser; +import uk.co.compendiumdev.thingifier.api.http.bodyparser.ApiBodyFields; 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; @@ -12,16 +12,16 @@ public class BodyCreationValidator { public ValidationReport validate( - final BodyParser bodyargs, final EntityDefinition thingDefinition) { + final ApiBodyFields bodyFields, 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(); + final Map bodyFieldValues = bodyFields.asMap(); for (String fieldName : notAllowedToCreateWithList) { - if (bodyFields.containsKey(fieldName)) { + if (bodyFieldValues.containsKey(fieldName)) { report.setValid(false); report.addErrorMessage(String.format("Not allowed to create with %s", fieldName)); } @@ -31,14 +31,14 @@ public ValidationReport validate( } public ValidationReport areFieldsUnique( - final BodyParser bodyargs, + final ApiBodyFields bodyFields, final EntityDefinition thingDefinition, final EntityInstanceQuery query, final List uniqueFields) { final ValidationReport report = new ValidationReport(); - for (Map.Entry entry : bodyargs.getFlattenedStringMap()) { + for (Map.Entry entry : bodyFields.asFlattenedStringMap()) { if (uniqueFields.contains(entry.getKey())) { String existingValue = entry.getValue(); 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..4353dd6f --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/DefaultThingifierApiRuntime.java @@ -0,0 +1,52 @@ +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(); + } + + @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()); + } + + @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 91% 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..491ad9f9 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,10 +1,8 @@ -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; @@ -15,19 +13,10 @@ public final class RelationshipBodyCommandParser { private final SchemaCatalog schema; - public RelationshipBodyCommandParser(final Thingifier thingifier) { - this(new ThingifierSchemaCatalog(thingifier)); - } - - RelationshipBodyCommandParser(final SchemaCatalog schema) { + public RelationshipBodyCommandParser(final SchemaCatalog 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) { List> relationshipEntries = new ArrayList<>(); 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/api/restapihandlers/SchemaCatalog.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/SchemaCatalog.java similarity index 78% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/SchemaCatalog.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/SchemaCatalog.java index 10d22aed..d118aae6 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/SchemaCatalog.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/SchemaCatalog.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.core.domain.definitions.EntityDefinition; diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingBodyCommandMapper.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingBodyCommandMapper.java similarity index 64% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingBodyCommandMapper.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingBodyCommandMapper.java index 26adc794..53d9ca29 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingBodyCommandMapper.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingBodyCommandMapper.java @@ -1,18 +1,18 @@ -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.api.http.bodyparser.ApiBodyFields; import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; +import uk.co.compendiumdev.thingifier.application.EntityInstanceDraftBuilder; 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.application.command.PutThingCommand; 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; @@ -23,15 +23,7 @@ public final class ThingBodyCommandMapper { 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( + public ThingBodyCommandMapper( final SchemaCatalog schema, final ThingifierApiConfig apiConfig, final ThingStore store) { @@ -41,15 +33,15 @@ public ThingBodyCommandMapper(final Thingifier thingifier, final String database } public ThingWriteRequestMapping mapCreate( - final BodyParser bodyargs, + final ApiBodyFields bodyFields, final EntityDefinition entity, final boolean validateFinalRelationships) { - RelationshipBodyCommands relationships = parseRelationships(bodyargs, entity); + RelationshipBodyCommands relationships = parseRelationships(bodyFields, entity); if (!relationships.validationReport().isValid()) { return invalidRelationships(relationships.validationReport()); } - ValidationReport validated = new BodyCreationValidator().validate(bodyargs, entity); + ValidationReport validated = new BodyCreationValidator().validate(bodyFields, entity); if (!validated.isValid()) { return ThingWriteRequestMapping.error( ApiMappingError.withMessage( @@ -59,7 +51,7 @@ public ThingWriteRequestMapping mapCreate( } return mapCreateFromValidatedBody( - bodyargs, + bodyFields, entity, EntityInstanceDraft.forEntity(entity), relationships, @@ -67,11 +59,13 @@ public ThingWriteRequestMapping mapCreate( } public ThingWriteRequestMapping mapCreateWithPrimaryKey( - final String primaryKey, final BodyParser bodyargs, final EntityDefinition entity) { + final String primaryKey, + final ApiBodyFields bodyFields, + final EntityDefinition entity) { ValidationReport validated = new BodyCreationValidator() .areFieldsUnique( - bodyargs, + bodyFields, entity, store.entityQueries(), entity.getFieldNamesOfType( @@ -91,17 +85,17 @@ public ThingWriteRequestMapping mapCreateWithPrimaryKey( baseDraft.withField(entity.getPrimaryKeyField().getName(), primaryKey); } - RelationshipBodyCommands relationships = parseRelationships(bodyargs, entity); + RelationshipBodyCommands relationships = parseRelationships(bodyFields, entity); if (!relationships.validationReport().isValid()) { return invalidRelationships(relationships.validationReport()); } List fieldValues = - FieldValues.fromListMapEntryStringString(bodyargs.getFlattenedStringMap()); + FieldValues.fromListMapEntryStringString(bodyFields.asFlattenedStringMap()); store.administration().accommodateProtectedIds(entity, fieldValues); if (apiConfig.willApiEnforceDeclaredTypesInInput()) { - ValidationReport validatedTypes = bodyargs.validateAgainstType(entity); + ValidationReport validatedTypes = bodyFields.validateAgainstType(entity); if (!validatedTypes.isValid()) { return ThingWriteRequestMapping.error( ApiMappingError.withMessage( @@ -112,11 +106,11 @@ public ThingWriteRequestMapping mapCreateWithPrimaryKey( try { List ignoreFields = entity.getFieldNamesOfType(FieldType.AUTO_GUID); EntityInstanceDraft draft = - new EntityInstanceBulkUpdater(entity) + new EntityInstanceDraftBuilder(entity) .overrideFieldValuesFromArgsIgnoring( FieldValues.fromListMapEntryStringString( fieldValuesExcludingRelationships( - bodyargs, relationships)), + bodyFields, relationships)), ignoreFields); copyBaseDraftValues(baseDraft, draft); return ThingWriteRequestMapping.command( @@ -127,17 +121,24 @@ public ThingWriteRequestMapping mapCreateWithPrimaryKey( } public ThingWriteRequestMapping mapAmend( - final BodyParser bodyargs, - final EntityInstance instance, + final ApiBodyFields bodyFields, + final EntityDefinition entity, + final String identifier, final boolean replaceExistingFieldsAndRelationships) { - bodyargs.getMap(); + return mapAmend(bodyFields, entity, identifier, replaceExistingFieldsAndRelationships, ""); + } + public ThingWriteRequestMapping mapAmend( + final ApiBodyFields bodyFields, + final EntityDefinition entity, + final String identifier, + final boolean replaceExistingFieldsAndRelationships, + final String missingInstanceMessage) { if (apiConfig.willApiEnforceDeclaredTypesInInput()) { List doNotValidateFields = - instance.getEntity() - .getFieldNamesOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID); + entity.getFieldNamesOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID); ValidationReport validatedTypes = - bodyargs.validateAgainstTypeIgnoring(instance.getEntity(), doNotValidateFields); + bodyFields.validateAgainstTypeIgnoring(entity, doNotValidateFields); if (!validatedTypes.isValid()) { return ThingWriteRequestMapping.error( ApiMappingError.withMessage( @@ -145,16 +146,10 @@ public ThingWriteRequestMapping mapAmend( } } - 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())); - } + RelationshipBodyCommands relationships = parseRelationships(bodyFields, entity); + List fieldValues = + FieldValues.fromListMapEntryStringString( + fieldValuesExcludingRelationships(bodyFields, relationships)); if (!relationships.validationReport().isValid()) { return ThingWriteRequestMapping.error( @@ -164,18 +159,53 @@ public ThingWriteRequestMapping mapAmend( return ThingWriteRequestMapping.command( new AmendThingCommand( - instance, - draft, + entity, + identifier, + fieldValues, replaceExistingFieldsAndRelationships, - relationships.references())); + relationships.references(), + missingInstanceMessage)); + } + + public ThingWriteRequestMapping mapPut( + final ApiBodyFields bodyFields, + final EntityDefinition entity, + final String identifier) { + if (apiConfig.willApiEnforceDeclaredTypesInInput()) { + List doNotValidateFields = + entity.getFieldNamesOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID); + ValidationReport validatedTypes = + bodyFields.validateAgainstTypeIgnoring(entity, doNotValidateFields); + if (!validatedTypes.isValid()) { + return ThingWriteRequestMapping.error( + ApiMappingError.withMessage( + 400, validatedTypes.getCombinedErrorMessages())); + } + } + + RelationshipBodyCommands relationships = parseRelationships(bodyFields, entity); + List fieldValues = + FieldValues.fromListMapEntryStringString( + fieldValuesExcludingRelationships(bodyFields, relationships)); + + if (!relationships.validationReport().isValid()) { + return ThingWriteRequestMapping.error( + ApiMappingError.withMessages( + 400, relationships.validationReport().getErrorMessages())); + } + + return ThingWriteRequestMapping.command( + new PutThingCommand(entity, identifier, fieldValues, relationships.references())); } public ThingWriteRequestMapping mapCreateAndConnect( - final BodyParser bodyargs, - final EntityInstance parent, + final ApiBodyFields bodyFields, + final EntityDefinition parentEntity, + final String parentIdentifier, final String relationshipName, - final EntityDefinition childEntity) { - ThingWriteRequestMapping createMapping = mapCreate(bodyargs, childEntity, false); + final EntityDefinition childEntity, + final String routeDisplay) { + ThingWriteRequestMapping createMapping = mapCreate(bodyFields, childEntity, false); if (createMapping.isError()) { return createMapping; } @@ -183,17 +213,22 @@ public ThingWriteRequestMapping mapCreateAndConnect( CreateThingCommand create = (CreateThingCommand) createMapping.getCommand(); return ThingWriteRequestMapping.command( new CreateAndConnectRelationshipCommand( - parent, relationshipName, create.getDraft(), create.getRelationships())); + parentEntity, + parentIdentifier, + relationshipName, + create.getDraft(), + create.getRelationships(), + routeDisplay)); } private ThingWriteRequestMapping mapCreateFromValidatedBody( - final BodyParser bodyargs, + final ApiBodyFields bodyFields, final EntityDefinition entity, final EntityInstanceDraft baseDraft, final RelationshipBodyCommands relationships, final boolean validateFinalRelationships) { if (apiConfig.willApiEnforceDeclaredTypesInInput()) { - ValidationReport validatedTypes = bodyargs.validateAgainstType(entity); + ValidationReport validatedTypes = bodyFields.validateAgainstType(entity); if (!validatedTypes.isValid()) { return ThingWriteRequestMapping.error( ApiMappingError.withMessage( @@ -204,9 +239,9 @@ private ThingWriteRequestMapping mapCreateFromValidatedBody( try { List fieldValues = FieldValues.fromListMapEntryStringString( - fieldValuesExcludingRelationships(bodyargs, relationships)); + fieldValuesExcludingRelationships(bodyFields, relationships)); EntityInstanceDraft draft = - new EntityInstanceBulkUpdater(entity).setFieldValuesFrom(fieldValues); + new EntityInstanceDraftBuilder(entity).setFieldValuesFrom(fieldValues); copyBaseDraftValues(baseDraft, draft); return ThingWriteRequestMapping.command( new CreateThingCommand( @@ -217,8 +252,9 @@ private ThingWriteRequestMapping mapCreateFromValidatedBody( } private RelationshipBodyCommands parseRelationships( - final BodyParser bodyargs, final EntityDefinition entity) { - return new RelationshipBodyCommandParser(schema).parse(bodyargs, entity); + final ApiBodyFields bodyFields, final EntityDefinition entity) { + return new RelationshipBodyCommandParser(schema) + .parse(bodyFields.asFlattenedStringMap(), entity); } private ThingWriteRequestMapping invalidRelationships(final ValidationReport validation) { @@ -231,9 +267,9 @@ private ThingWriteRequestMapping invalidRelationships(final ValidationReport val } private List> fieldValuesExcludingRelationships( - final BodyParser bodyargs, final RelationshipBodyCommands relationships) { + final ApiBodyFields bodyFields, final RelationshipBodyCommands relationships) { List> flattenedArgs = - new ArrayList<>(bodyargs.getFlattenedStringMap()); + new ArrayList<>(bodyFields.asFlattenedStringMap()); flattenedArgs.removeAll(relationships.relationshipEntries()); return flattenedArgs; } diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingCommandResultApiMapper.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingCommandResultApiMapper.java similarity index 82% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingCommandResultApiMapper.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingCommandResultApiMapper.java index 0d49f7b0..5101d494 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingCommandResultApiMapper.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingCommandResultApiMapper.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.api.response.ApiResponse; import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; @@ -9,6 +9,7 @@ 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.PutThingCommand; import uk.co.compendiumdev.thingifier.application.command.ThingWriteCommand; public final class ThingCommandResultApiMapper { @@ -40,6 +41,13 @@ public ApiResponse map(final ThingWriteCommand command, final ThingCommandResult return ApiResponse.created(null, apiConfig); } + if (command instanceof PutThingCommand) { + 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()); } @@ -57,9 +65,11 @@ private ApiResponse errorResponseFor( ApiResponse response; if (command instanceof CreateThingCommand && result.getErrorMessages().size() == 1) { response = - ApiResponse.error(400, creationErrorMessage(result.getCombinedErrorMessage())); + ApiResponse.error( + result.statusCode(), + creationErrorMessage(result.getCombinedErrorMessage())); } else { - response = ApiResponse.error(400, result.getErrorMessages()); + response = ApiResponse.error(result.statusCode(), result.getErrorMessages()); } if (result.rolledBackCreatedInstance()) { 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..114709ee --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingReadRequestMapper.java @@ -0,0 +1,63 @@ +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.core.domain.definitions.EntityDefinition; +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(), 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(), identifierCandidate, queryParams)); + } + + if (route instanceof RelationshipCollectionRoute) { + RelationshipCollectionRoute relationship = (RelationshipCollectionRoute) route; + return ThingReadRequestMapping.query( + new ReadRelationshipQuery( + relationship.parentEntity(), + 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 EntityDefinition entityForTerm(final String term) { + return schema.definitionWithSingularOrPluralNamed(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..8dcc32d2 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingWriteRequestMapper.java @@ -0,0 +1,180 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; + +import java.util.List; +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.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.repository.ThingStore; + +public final class ThingWriteRequestMapper { + + private final ThingBodyCommandMapper bodyCommandMapper; + + public ThingWriteRequestMapper( + final SchemaCatalog schema, + final ThingifierApiConfig apiConfig, + final ThingStore store) { + this.bodyCommandMapper = new ThingBodyCommandMapper(schema, apiConfig, store); + } + + 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(), instance.identifier(), route.originalPath())); + } + + if (route instanceof RelationshipInstanceRoute) { + RelationshipInstanceRoute relationship = (RelationshipInstanceRoute) route; + return ThingWriteRequestMapping.command( + new DisconnectRelationshipCommand( + relationship.parentEntity(), + relationship.parentIdentifier(), + relationship.relationshipName(), + relationship.childIdentifier(), + 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()) { + return bodyCommandMapper.mapAmend( + bodyFields, + route.entity(), + route.identifier(), + false, + String.format( + "No such %s entity instance with %s == %s found", + route.entity().getName(), + route.entity().getPrimaryKeyField().getName(), + route.identifier())); + } + + return ThingWriteRequestMapping.error( + ApiMappingError.withMessage( + 404, + String.format( + "Entity %s does not have a primary key defined", + route.entity().getName()))); + } + + private ThingWriteRequestMapping mapPostToRelationship( + final RelationshipCollectionRoute route, final ApiBodyFields bodyFields) { + List possibleRelationships = + route.parentEntity().related().getRelationships(route.relationshipName()); + RelationshipVectorDefinition relationshipToUse = possibleRelationships.get(0); + EntityDefinition thingTo = relationshipToUse.getTo(); + + List childReferenceFields = + FieldValues.fromListMapEntryStringString(bodyFields.asFlattenedStringMap()); + if (bodyReferencesExistingRelatedItem(thingTo, childReferenceFields)) { + return ThingWriteRequestMapping.command( + new ConnectExistingRelationshipCommand( + route.parentEntity(), + route.parentIdentifier(), + route.relationshipName(), + childReferenceFields, + route.originalPath())); + } + + return bodyCommandMapper.mapCreateAndConnect( + bodyFields, + route.parentEntity(), + route.parentIdentifier(), + route.relationshipName(), + relationshipToUse.getTo(), + route.originalPath()); + } + + private 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; + } +} diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingWriteRequestMapping.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingWriteRequestMapping.java similarity index 93% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingWriteRequestMapping.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingWriteRequestMapping.java index cf099e54..ccae332f 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingWriteRequestMapping.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingWriteRequestMapping.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.command.ThingWriteCommand; 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/api/restapihandlers/ThingifierSchemaCatalog.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingifierSchemaCatalog.java similarity index 74% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingifierSchemaCatalog.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingifierSchemaCatalog.java index 7b3014cc..d8545f5c 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingifierSchemaCatalog.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingifierSchemaCatalog.java @@ -1,13 +1,13 @@ -package uk.co.compendiumdev.thingifier.api.restapihandlers; +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; import uk.co.compendiumdev.thingifier.Thingifier; import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -final class ThingifierSchemaCatalog implements SchemaCatalog { +public final class ThingifierSchemaCatalog implements SchemaCatalog { private final Thingifier thingifier; - ThingifierSchemaCatalog(final Thingifier thingifier) { + public ThingifierSchemaCatalog(final Thingifier thingifier) { this.thingifier = thingifier; } 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..48f161e1 --- /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.core.domain.definitions.EntityDefinition; + +public final class CollectionRoute extends ThingRoute { + + private final EntityDefinition entity; + + CollectionRoute(final String originalPath, final EntityDefinition entity) { + super(originalPath); + this.entity = entity; + } + + public EntityDefinition 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..b6587371 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/InstanceRoute.java @@ -0,0 +1,24 @@ +package uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route; + +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; + +public final class InstanceRoute extends ThingRoute { + + private final EntityDefinition entity; + private final String identifier; + + InstanceRoute( + final String originalPath, final EntityDefinition entity, final String identifier) { + super(originalPath); + this.entity = entity; + this.identifier = identifier; + } + + public EntityDefinition 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..c7becd0e --- /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.core.domain.definitions.EntityDefinition; + +public final class RelationshipCollectionRoute extends ThingRoute { + + private final EntityDefinition parentEntity; + private final String parentIdentifier; + private final String relationshipName; + + RelationshipCollectionRoute( + final String originalPath, + final EntityDefinition parentEntity, + final String parentIdentifier, + final String relationshipName) { + super(originalPath); + this.parentEntity = parentEntity; + this.parentIdentifier = parentIdentifier; + this.relationshipName = relationshipName; + } + + public EntityDefinition 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..e69663ab --- /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.core.domain.definitions.EntityDefinition; + +public final class RelationshipInstanceRoute extends ThingRoute { + + private final EntityDefinition parentEntity; + private final String parentIdentifier; + private final String relationshipName; + private final String childIdentifier; + + RelationshipInstanceRoute( + final String originalPath, + final EntityDefinition 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 EntityDefinition 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..3319547c --- /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.adapter.http.apihandlers.SchemaCatalog; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; + +public final class ThingRouteMapper { + + private final SchemaCatalog schema; + + public ThingRouteMapper(final SchemaCatalog 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); + } + + EntityDefinition entity = schema.definitionWithSingularOrPluralNamed(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.related().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/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/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..5411920f --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/bodyparser/ApiBodyField.java @@ -0,0 +1,20 @@ +package uk.co.compendiumdev.thingifier.api.http.bodyparser; + +public final class ApiBodyField { + + private final String name; + private final String value; + + public ApiBodyField(final String name, final String value) { + this.name = name; + this.value = value; + } + + public String name() { + return name; + } + + public String value() { + return value; + } +} 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..f1683d83 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/bodyparser/ApiBodyFields.java @@ -0,0 +1,168 @@ +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; +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.reporting.ValidationReport; + +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); + } + + 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; + } + + public ValidationReport validateAgainstType(final EntityDefinition entity) { + return validateAgainstTypeIgnoring(entity, new ArrayList<>()); + } + + public ValidationReport validateAgainstTypeIgnoring( + final EntityDefinition entity, final List doNotValidateFields) { + ValidationReport report = new ValidationReport(); + for (Map.Entry fieldValue : fields.entrySet()) { + + if (entity.hasAnyOfFieldNamesDefined(doNotValidateFields)) { + continue; + } + + Field field = entity.getField(fieldValue.getKey()); + if (field == null) { + continue; + } + + Object value = fieldValue.getValue(); + String instanceType = "Something Else"; + if (value instanceof String) { + instanceType = "STRING"; + } + if (value instanceof Boolean) { + instanceType = "BOOLEAN"; + } + if (value instanceof Integer) { + instanceType = "INTEGER"; + } + if (value instanceof Float) { + instanceType = "NUMERIC"; + } + if (value instanceof Double) { + instanceType = "NUMERIC"; + } + + String errorMessage = + String.format( + "%s should be %s but was %s", + field.getName(), field.getType(), instanceType); + + if (field.getType() == FieldType.BOOLEAN && !(value instanceof Boolean)) { + report.setValid(false); + report.addErrorMessage(errorMessage); + } + if (field.getType() == FieldType.INTEGER + || field.getType() == FieldType.AUTO_INCREMENT) { + if (!(value instanceof Double)) { + report.setValid(false); + report.addErrorMessage(errorMessage); + } else { + fieldValue.setValue(((Double) value).intValue()); + } + } + if (field.getType() == FieldType.FLOAT && !(value instanceof Double)) { + report.setValid(false); + report.addErrorMessage(errorMessage); + } + } + return report; + } +} 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/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..e16e03aa 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(), runtime.apiConfig(), context.store()) + .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()); + ThingCommandResult result = runtime.commandService(context).execute(mapping.getCommand()); return apiMapper.map(mapping.getCommand(), 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..ad1bd91d 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(), runtime.apiConfig(), context.store()) + .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()); + ThingCommandResult result = runtime.commandService(context).execute(mapping.getCommand()); return apiMapper.map(mapping.getCommand(), 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..faf879a6 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(), runtime.apiConfig(), context.store()) + .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()); + ThingCommandResult result = runtime.commandService(context).execute(mapping.getCommand()); return apiMapper.map(mapping.getCommand(), result); } } 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/application/ApplicationError.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ApplicationError.java new file mode 100644 index 00000000..d233b132 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ApplicationError.java @@ -0,0 +1,59 @@ +package uk.co.compendiumdev.thingifier.application; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public final class ApplicationError { + + public enum Category { + VALIDATION, + NOT_FOUND, + CONFLICT, + UNSUPPORTED + } + + private final Category category; + private final int statusCode; + private final List messages; + + private ApplicationError( + final Category category, final int statusCode, final Collection messages) { + this.category = category; + this.statusCode = statusCode; + this.messages = Collections.unmodifiableList(new ArrayList<>(messages)); + } + + public static ApplicationError validation(final String message) { + return validation(List.of(message)); + } + + public static ApplicationError validation(final Collection messages) { + return new ApplicationError(Category.VALIDATION, 400, messages); + } + + public static ApplicationError notFound(final String message) { + return new ApplicationError(Category.NOT_FOUND, 404, List.of(message)); + } + + public static ApplicationError conflict(final String message) { + return new ApplicationError(Category.CONFLICT, 409, List.of(message)); + } + + public static ApplicationError unsupported(final String message) { + return new ApplicationError(Category.UNSUPPORTED, 400, List.of(message)); + } + + public Category category() { + return category; + } + + public int statusCode() { + return statusCode; + } + + public List messages() { + return messages; + } +} 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/ThingCommandResult.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingCommandResult.java index 4b21193c..afd1a212 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,19 @@ public String getCombinedErrorMessage() { public boolean rolledBackCreatedInstance() { return rolledBackCreatedInstance; } + + public ApplicationError getError() { + return error; + } + + public int statusCode() { + if (error == null) { + return 200; + } + return error.statusCode(); + } + + 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..653520ea 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 @@ -10,12 +10,14 @@ 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.PutThingCommand; import uk.co.compendiumdev.thingifier.application.command.RelationshipReference; 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.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; @@ -41,22 +43,21 @@ public ThingCommandResult execute(final ThingWriteCommand command) { if (command instanceof AmendThingCommand) { AmendThingCommand amend = (AmendThingCommand) command; - return amend( - amend.getInstance(), - amend.getDraft(), - amend.shouldReplaceExistingFieldsAndRelationships(), - amend.getRelationships()); + return amend(amend); } if (command instanceof DeleteThingCommand) { - return delete(((DeleteThingCommand) command).getInstance()); + return delete((DeleteThingCommand) command); + } + + if (command instanceof PutThingCommand) { + return put((PutThingCommand) command); } if (command instanceof ConnectExistingRelationshipCommand) { ConnectExistingRelationshipCommand connect = (ConnectExistingRelationshipCommand) command; - return connectRelationship( - connect.getParent(), connect.getRelationshipName(), connect.getChild(), false); + return connectExistingRelationship(connect); } if (command instanceof CreateAndConnectRelationshipCommand) { @@ -65,14 +66,13 @@ public ThingCommandResult execute(final ThingWriteCommand command) { if (command instanceof DisconnectRelationshipCommand) { DisconnectRelationshipCommand disconnect = (DisconnectRelationshipCommand) command; - return disconnectRelationship( - disconnect.getParent(), - disconnect.getChild(), - disconnect.getRelationshipName()); + return disconnectRelationship(disconnect); } return ThingCommandResult.error( - String.format("Unsupported command %s", command.getClass().getSimpleName())); + ApplicationError.unsupported( + String.format( + "Unsupported command %s", command.getClass().getSimpleName()))); } public ThingCommandResult create( @@ -136,6 +136,116 @@ public ThingCommandResult amend( } } + private ThingCommandResult amend(final AmendThingCommand command) { + if (command.hasResolvedInstance()) { + return amend( + command.getInstance(), + command.getDraft(), + command.shouldReplaceExistingFieldsAndRelationships(), + command.getRelationships()); + } + + EntityInstance instance = resolveInstance(command.getEntity(), command.getIdentifier()); + if (instance == null) { + String message = command.getMissingInstanceMessage(); + if (message == null || message.isEmpty()) { + message = + String.format( + "Could not find any instances with %s", command.getIdentifier()); + } + return ThingCommandResult.error(ApplicationError.notFound(message)); + } + + try { + EntityInstanceDraft draft = + new EntityInstanceDraftBuilder(instance) + .setFieldValuesFrom(command.getFieldValues()); + return amend( + instance, + draft, + command.shouldReplaceExistingFieldsAndRelationships(), + command.getRelationships()); + } catch (Exception e) { + return ThingCommandResult.error(messageFrom(e)); + } + } + + private ThingCommandResult put(final PutThingCommand command) { + EntityInstance instance = resolveInstance(command.getEntity(), command.getIdentifier()); + if (instance != null) { + try { + EntityInstanceDraft draft = + new EntityInstanceDraftBuilder(instance) + .setFieldValuesFrom(command.getFieldValues()); + return amend(instance, draft, true, command.getRelationships()); + } catch (Exception e) { + return ThingCommandResult.error(messageFrom(e)); + } + } + + try { + ThingCommandResult creationAllowed = validatePutCreate(command); + if (creationAllowed != null) { + return creationAllowed; + } + EntityInstanceDraft draft = createDraftWithPrimaryKey(command); + ThingCommandResult created = create(draft, command.getRelationships(), true); + if (created.isError()) { + return created; + } + return ThingCommandResult.created(created.getInstance()); + } catch (Exception e) { + return ThingCommandResult.error(messageFrom(e)); + } + } + + private ThingCommandResult validatePutCreate(final PutThingCommand command) { + List forbiddenPutCreationFields = + command.getEntity().getFieldsOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID); + if (!forbiddenPutCreationFields.isEmpty()) { + return ThingCommandResult.error( + String.format( + "Cannot create %s with PUT due to Auto fields %s", + command.getEntity().getName(), fieldNames(forbiddenPutCreationFields))); + } + + Field primaryKey = command.getEntity().getPrimaryKeyField(); + for (NamedValue namedValue : command.getFieldValues()) { + if (namedValue.name.equals(primaryKey.getName()) + && !namedValue.value.equals(command.getIdentifier())) { + return ThingCommandResult.error( + String.format( + "Cannot create %s with PUT as key does not match body value %s != %s", + command.getEntity().getName(), + command.getIdentifier(), + namedValue.value)); + } + } + + return null; + } + + private EntityInstanceDraft createDraftWithPrimaryKey(final PutThingCommand command) { + EntityInstanceDraft baseDraft = EntityInstanceDraft.forEntity(command.getEntity()); + Field primaryKeyField = command.getEntity().getPrimaryKeyField(); + if (primaryKeyField.getType() == FieldType.AUTO_INCREMENT + || primaryKeyField.getType() == FieldType.AUTO_GUID) { + baseDraft.withProtectedField(primaryKeyField.getName(), command.getIdentifier()); + } else { + baseDraft.withField(primaryKeyField.getName(), command.getIdentifier()); + } + + List fieldValues = new ArrayList<>(command.getFieldValues()); + store.administration().accommodateProtectedIds(command.getEntity(), fieldValues); + EntityInstanceDraft draft = + new EntityInstanceDraftBuilder(command.getEntity()) + .overrideFieldValuesFromArgsIgnoring( + fieldValues, + command.getEntity().getFieldNamesOfType(FieldType.AUTO_GUID)); + copyBaseDraftValues(baseDraft, draft); + return draft; + } + public ThingCommandResult delete(final EntityInstance instance) { try { store.entities().delete(instance); @@ -145,6 +255,22 @@ public ThingCommandResult delete(final EntityInstance instance) { } } + private ThingCommandResult delete(final DeleteThingCommand command) { + if (command.hasResolvedInstance()) { + return delete(command.getInstance()); + } + + EntityInstance instance = resolveInstance(command.getEntity(), command.getIdentifier()); + if (instance == null) { + return ThingCommandResult.error( + ApplicationError.notFound( + String.format( + "Could not find any instances with %s", + command.getRouteDisplay()))); + } + return delete(instance); + } + public ThingCommandResult connectRelationship( final EntityInstance parent, final String relationshipName, @@ -190,12 +316,46 @@ public ThingCommandResult disconnectRelationship( } } + private ThingCommandResult disconnectRelationship(final DisconnectRelationshipCommand command) { + if (command.hasResolvedRelationship()) { + return disconnectRelationship( + command.getParent(), command.getChild(), command.getRelationshipName()); + } + + EntityInstance parent = + resolveInstance(command.getParentEntity(), command.getParentIdentifier()); + if (parent == null) { + return relationshipRouteNotFound(command.getRouteDisplay()); + } + + EntityInstance child = + relatedInstanceMatchingIdentifier( + parent, command.getRelationshipName(), command.getChildIdentifier()); + if (child == null) { + return relationshipRouteNotFound(command.getRouteDisplay()); + } + + return disconnectRelationship(parent, child, command.getRelationshipName()); + } + public ThingCommandResult connectRelationships( final EntityInstance instance, final List relationships) { return connectRelationshipReferences(instance, relationships, true, false); } private ThingCommandResult createAndConnect(final CreateAndConnectRelationshipCommand command) { + EntityInstance parent = command.getParent(); + if (!command.hasResolvedParent()) { + parent = resolveInstance(command.getParentEntity(), command.getParentIdentifier()); + if (parent == null) { + return ThingCommandResult.error( + ApplicationError.notFound( + String.format( + "Could not find parent thing for relationship %s", + command.getRouteDisplay()))); + } + } + ThingCommandResult createResult = create(command.getChildDraft(), command.getChildRelationships(), false); if (createResult.isError()) { @@ -204,10 +364,7 @@ private ThingCommandResult createAndConnect(final CreateAndConnectRelationshipCo ThingCommandResult connectResult = connectRelationship( - command.getParent(), - command.getRelationshipName(), - createResult.getInstance(), - true); + parent, command.getRelationshipName(), createResult.getInstance(), true); if (connectResult.isError()) { return connectResult.withRolledBackCreatedInstance(); } @@ -215,6 +372,44 @@ private ThingCommandResult createAndConnect(final CreateAndConnectRelationshipCo return ThingCommandResult.success(createResult.getInstance()); } + private ThingCommandResult connectExistingRelationship( + final ConnectExistingRelationshipCommand command) { + if (command.hasResolvedRelationship()) { + return connectRelationship( + command.getParent(), command.getRelationshipName(), command.getChild(), false); + } + + EntityInstance parent = + resolveInstance(command.getParentEntity(), command.getParentIdentifier()); + if (parent == null) { + return ThingCommandResult.error( + ApplicationError.notFound( + String.format( + "Could not find parent thing for relationship %s", + command.getRouteDisplay()))); + } + + RelatedItemResolution related = + 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 = + relationshipErrorIfInvalid( + parent, related.instance, relationshipToUse, command.getRelationshipName()); + if (relationshipError != null) { + return relationshipError; + } + + return connectRelationship(parent, relationshipToUse.getName(), related.instance, false); + } + private ThingCommandResult connectRelationships( final EntityInstance instance, final List relationships, @@ -283,6 +478,122 @@ private RelationshipResolution resolveRelationshipReferences( return new RelationshipResolution(relationships, errors); } + private EntityInstance resolveInstance( + final EntityDefinition entity, final String queryIdentifier) { + return store.entityQueries().findByQueryIdentifier(entity, queryIdentifier); + } + + private ThingCommandResult relationshipRouteNotFound(final String routeDisplay) { + return ThingCommandResult.error( + ApplicationError.notFound( + String.format("Could not find any instances with %s", routeDisplay))); + } + + private 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); + } + + private 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 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); + } + private EntityInstance resolveRelationshipReference( final EntityInstance instance, final RelationshipReference reference) { if (reference.hasExplicitTargetEntity()) { @@ -396,6 +707,27 @@ private EntityInstanceDraft draftFrom(final EntityInstance instance) { return draft; } + 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 String fieldNames(final List fields) { + String names = ""; + for (Field field : fields) { + if (!names.isEmpty()) { + names = names + ", "; + } + names = names + field.getName(); + } + return names; + } + private String messageFrom(final Exception exception) { String message = exception.getMessage(); return message == null ? "" : message; @@ -417,6 +749,25 @@ private boolean hasErrors() { } } + private 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; + } + + private static RelatedItemResolution success(final EntityInstance instance) { + return new RelatedItemResolution(instance, null); + } + + private static RelatedItemResolution error(final ApplicationError error) { + return new RelatedItemResolution(null, error); + } + } + private static final class RelationshipSnapshot { private final List links; 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..a3f9d47b 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,15 +3,21 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +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; public final class AmendThingCommand implements ThingWriteCommand { private final EntityInstance instance; + private final EntityDefinition entity; + private final String identifier; private final EntityInstanceDraft draft; + private final List fieldValues; private final boolean replaceExistingFieldsAndRelationships; private final List relationships; + private final String missingInstanceMessage; public AmendThingCommand( final EntityInstance instance, @@ -19,19 +25,71 @@ public AmendThingCommand( final boolean replaceExistingFieldsAndRelationships, final List relationships) { this.instance = instance; + this.entity = instance.getEntity(); + this.identifier = instance.getPrimaryKeyValue(); this.draft = draft; + this.fieldValues = Collections.emptyList(); this.replaceExistingFieldsAndRelationships = replaceExistingFieldsAndRelationships; this.relationships = Collections.unmodifiableList(new ArrayList<>(relationships)); + this.missingInstanceMessage = ""; + } + + public AmendThingCommand( + final EntityDefinition entity, + final String identifier, + final List fieldValues, + final boolean replaceExistingFieldsAndRelationships, + final List relationships) { + this( + entity, + identifier, + fieldValues, + replaceExistingFieldsAndRelationships, + relationships, + ""); + } + + public AmendThingCommand( + final EntityDefinition entity, + final String identifier, + final List fieldValues, + final boolean replaceExistingFieldsAndRelationships, + final List relationships, + final String missingInstanceMessage) { + this.instance = null; + this.entity = entity; + this.identifier = identifier; + this.draft = null; + this.fieldValues = Collections.unmodifiableList(new ArrayList<>(fieldValues)); + this.replaceExistingFieldsAndRelationships = replaceExistingFieldsAndRelationships; + this.relationships = Collections.unmodifiableList(new ArrayList<>(relationships)); + this.missingInstanceMessage = missingInstanceMessage; } public EntityInstance getInstance() { return instance; } + public boolean hasResolvedInstance() { + return instance != null; + } + + public EntityDefinition getEntity() { + return entity; + } + + public String getIdentifier() { + return identifier; + } + public EntityInstanceDraft getDraft() { return draft; } + public List getFieldValues() { + return fieldValues; + } + public boolean shouldReplaceExistingFieldsAndRelationships() { return replaceExistingFieldsAndRelationships; } @@ -39,4 +97,8 @@ public boolean shouldReplaceExistingFieldsAndRelationships() { public List getRelationships() { return relationships; } + + public String getMissingInstanceMessage() { + return missingInstanceMessage; + } } 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..7200f91b 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,26 +1,67 @@ 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.EntityDefinition; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; public final class ConnectExistingRelationshipCommand implements ThingWriteCommand { private final EntityInstance parent; + private final EntityDefinition parentEntity; + private final String parentIdentifier; private final String relationshipName; private final EntityInstance child; + private final List childReferenceFields; + private final String routeDisplay; public ConnectExistingRelationshipCommand( final EntityInstance parent, final String relationshipName, final EntityInstance child) { this.parent = parent; + this.parentEntity = parent.getEntity(); + this.parentIdentifier = parent.getPrimaryKeyValue(); this.relationshipName = relationshipName; this.child = child; + this.childReferenceFields = Collections.emptyList(); + this.routeDisplay = ""; + } + + public ConnectExistingRelationshipCommand( + final EntityDefinition parentEntity, + final String parentIdentifier, + final String relationshipName, + final List childReferenceFields, + final String routeDisplay) { + this.parent = null; + this.parentEntity = parentEntity; + this.parentIdentifier = parentIdentifier; + this.relationshipName = relationshipName; + this.child = null; + this.childReferenceFields = + Collections.unmodifiableList(new ArrayList<>(childReferenceFields)); + this.routeDisplay = routeDisplay; } public EntityInstance getParent() { return parent; } + public boolean hasResolvedRelationship() { + return parent != null && child != null; + } + + public EntityDefinition getParentEntity() { + return parentEntity; + } + + public String getParentIdentifier() { + return parentIdentifier; + } + public String getRelationshipName() { return relationshipName; } @@ -28,4 +69,12 @@ public String getRelationshipName() { public EntityInstance getChild() { return child; } + + public List getChildReferenceFields() { + return childReferenceFields; + } + + public String getRouteDisplay() { + return routeDisplay; + } } 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..d261c9ae 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,15 +3,19 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft; public final class CreateAndConnectRelationshipCommand implements ThingWriteCommand { private final EntityInstance parent; + private final EntityDefinition parentEntity; + private final String parentIdentifier; private final String relationshipName; private final EntityInstanceDraft childDraft; private final List childRelationships; + private final String routeDisplay; public CreateAndConnectRelationshipCommand( final EntityInstance parent, @@ -19,15 +23,46 @@ public CreateAndConnectRelationshipCommand( final EntityInstanceDraft childDraft, final List childRelationships) { this.parent = parent; + this.parentEntity = parent.getEntity(); + this.parentIdentifier = parent.getPrimaryKeyValue(); this.relationshipName = relationshipName; this.childDraft = childDraft; this.childRelationships = Collections.unmodifiableList(new ArrayList<>(childRelationships)); + this.routeDisplay = ""; + } + + public CreateAndConnectRelationshipCommand( + final EntityDefinition parentEntity, + final String parentIdentifier, + final String relationshipName, + final EntityInstanceDraft childDraft, + final List childRelationships, + final String routeDisplay) { + this.parent = null; + this.parentEntity = parentEntity; + this.parentIdentifier = parentIdentifier; + this.relationshipName = relationshipName; + this.childDraft = childDraft; + this.childRelationships = Collections.unmodifiableList(new ArrayList<>(childRelationships)); + this.routeDisplay = routeDisplay; } public EntityInstance getParent() { return parent; } + public boolean hasResolvedParent() { + return parent != null; + } + + public EntityDefinition getParentEntity() { + return parentEntity; + } + + public String getParentIdentifier() { + return parentIdentifier; + } + public String getRelationshipName() { return relationshipName; } @@ -39,4 +74,8 @@ public EntityInstanceDraft getChildDraft() { public List getChildRelationships() { return childRelationships; } + + public String getRouteDisplay() { + return routeDisplay; + } } 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..3bc943c5 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,47 @@ package uk.co.compendiumdev.thingifier.application.command; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; public final class DeleteThingCommand implements ThingWriteCommand { private final EntityInstance instance; + private final EntityDefinition entity; + private final String identifier; + private final String routeDisplay; public DeleteThingCommand(final EntityInstance instance) { this.instance = instance; + this.entity = instance.getEntity(); + this.identifier = instance.getPrimaryKeyValue(); + this.routeDisplay = ""; + } + + public DeleteThingCommand( + final EntityDefinition entity, final String identifier, final String routeDisplay) { + this.instance = null; + this.entity = entity; + this.identifier = identifier; + this.routeDisplay = routeDisplay; } public EntityInstance getInstance() { return instance; } + + public boolean hasResolvedInstance() { + return instance != null; + } + + public EntityDefinition getEntity() { + return entity; + } + + public String getIdentifier() { + return identifier; + } + + public String getRouteDisplay() { + return routeDisplay; + } } 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..56c1c7c7 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,12 +1,17 @@ package uk.co.compendiumdev.thingifier.application.command; +import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; 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 EntityDefinition parentEntity; + private final String parentIdentifier; private final String relationshipName; + private final String childIdentifier; + private final String routeDisplay; public DisconnectRelationshipCommand( final EntityInstance parent, @@ -14,7 +19,26 @@ public DisconnectRelationshipCommand( final String relationshipName) { this.parent = parent; this.child = child; + this.parentEntity = parent.getEntity(); + this.parentIdentifier = parent.getPrimaryKeyValue(); this.relationshipName = relationshipName; + this.childIdentifier = child.getPrimaryKeyValue(); + this.routeDisplay = ""; + } + + public DisconnectRelationshipCommand( + final EntityDefinition parentEntity, + final String parentIdentifier, + final String relationshipName, + final String childIdentifier, + final String routeDisplay) { + this.parent = null; + this.child = null; + this.parentEntity = parentEntity; + this.parentIdentifier = parentIdentifier; + this.relationshipName = relationshipName; + this.childIdentifier = childIdentifier; + this.routeDisplay = routeDisplay; } public EntityInstance getParent() { @@ -25,7 +49,27 @@ public EntityInstance getChild() { return child; } + public boolean hasResolvedRelationship() { + return parent != null && child != null; + } + + public EntityDefinition getParentEntity() { + return parentEntity; + } + + public String getParentIdentifier() { + return parentIdentifier; + } + public String getRelationshipName() { return relationshipName; } + + public String getChildIdentifier() { + return childIdentifier; + } + + public String getRouteDisplay() { + return routeDisplay; + } } diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/PutThingCommand.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/PutThingCommand.java new file mode 100644 index 00000000..3cfc363e --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/PutThingCommand.java @@ -0,0 +1,42 @@ +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.EntityDefinition; +import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; + +public final class PutThingCommand implements ThingWriteCommand { + + private final EntityDefinition entity; + private final String identifier; + private final List fieldValues; + private final List relationships; + + public PutThingCommand( + final EntityDefinition entity, + final String identifier, + final List fieldValues, + final List relationships) { + this.entity = entity; + this.identifier = identifier; + this.fieldValues = Collections.unmodifiableList(new ArrayList<>(fieldValues)); + this.relationships = Collections.unmodifiableList(new ArrayList<>(relationships)); + } + + public EntityDefinition getEntity() { + return entity; + } + + public String getIdentifier() { + return identifier; + } + + public List getFieldValues() { + return fieldValues; + } + + public List getRelationships() { + return relationships; + } +} 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..1d104a77 --- /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().getName()); + } + + @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().getName()); + 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/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..bcfdd540 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; @@ -39,8 +42,8 @@ public void parsesCompressedRelationshipAsValidationAndExecutionCommand() { body.put("task-of.guid", projectInstance.getPrimaryKeyValue()); RelationshipBodyCommands commands = - new RelationshipBodyCommandParser(thingifier) - .parse(parserFor(thingifier, body), task); + new RelationshipBodyCommandParser(new ThingifierSchemaCatalog(thingifier)) + .parse(bodyFieldsFor(body).asFlattenedStringMap(), task); Assertions.assertTrue(commands.validationReport().isValid()); Assertions.assertEquals(1, commands.relationshipEntries().size()); @@ -65,18 +68,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 +108,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/ThingReadRequestMapperTest.java b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingReadRequestMapperTest.java index 1763abdf..e2b63921 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,7 +23,7 @@ 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); @@ -31,7 +35,7 @@ public void mapsCollectionRoute() { 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 +46,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,7 +58,7 @@ 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); @@ -64,8 +68,7 @@ public void mapsPluralEntityNames() { @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 +78,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 +86,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 +94,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 +103,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,7 +113,7 @@ 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); @@ -126,7 +127,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 +150,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..c6cd817f 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,24 @@ 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.PutThingCommand; 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 +34,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 +49,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,61 +67,39 @@ 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 PutThingCommand); } @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 PutThingCommand); } @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 @@ -126,8 +111,11 @@ 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); @@ -141,8 +129,11 @@ public void mapsRelationshipPostWithoutChildKeyToCreateAndConnectCommand() { 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); @@ -155,7 +146,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 +166,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 +183,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 +231,27 @@ 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), + thingifier.apiConfig(), + storeFor(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/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(); From 27390a76f2a9aa0bfd88d2d147b08a0b2fb1b73f Mon Sep 17 00:00:00 2001 From: Alan Richardson Date: Tue, 14 Jul 2026 16:04:19 +0100 Subject: [PATCH 2/6] Refactor API mappers to schema references --- checkstyle-project-fqn.xml | 19 + .../core/repository/ThingStore.java | 2 + .../repository/ThingStoreTransaction.java | 11 + .../InMemoryEntityInstanceCollection.java | 37 + .../inmemory/InMemoryEntityInstanceStore.java | 34 + .../inmemory/InMemoryRelationshipStore.java | 18 + .../inmemory/InMemoryThingStore.java | 42 + .../repository/sqlite/SqliteThingStore.java | 117 +++ .../repository/ThingStoreContractTest.java | 63 ++ .../apihandlers/BodyCreationValidator.java | 69 -- .../DefaultThingifierApiRuntime.java | 7 +- .../RelationshipBodyCommandParser.java | 58 +- .../http/apihandlers/SchemaCatalog.java | 10 +- .../apihandlers/ThingBodyCommandMapper.java | 248 ++---- .../ThingCommandResultApiMapper.java | 25 +- .../apihandlers/ThingReadRequestMapper.java | 13 +- .../apihandlers/ThingWriteRequestMapper.java | 95 +-- .../apihandlers/ThingifierSchemaCatalog.java | 53 ++ .../apihandlers/route/CollectionRoute.java | 8 +- .../http/apihandlers/route/InstanceRoute.java | 9 +- .../route/RelationshipCollectionRoute.java | 8 +- .../route/RelationshipInstanceRoute.java | 8 +- .../apihandlers/route/ThingRouteMapper.java | 12 +- .../api/http/bodyparser/ApiBodyField.java | 10 + .../api/http/bodyparser/ApiBodyFields.java | 94 +-- .../restapihandlers/RestApiDeleteHandler.java | 2 +- .../restapihandlers/RestApiPostHandler.java | 2 +- .../restapihandlers/RestApiPutHandler.java | 2 +- .../application/AmendThingHandler.java | 21 + .../application/CreateThingHandler.java | 16 + .../RelationshipCommandHandler.java | 31 + .../RelationshipReferenceResolver.java | 125 +++ .../application/ThingCommandService.java | 755 +++++++++--------- .../application/ThingQueryService.java | 17 +- .../application/WriteTransactionRunner.java | 33 + .../application/WriteValidationPolicy.java | 230 ++++++ .../command/AmendThingCommand.java | 69 +- .../application/command/BodyFieldValue.java | 62 ++ .../ConnectExistingRelationshipCommand.java | 43 +- .../CreateAndConnectRelationshipCommand.java | 75 +- .../command/CreateThingCommand.java | 66 +- .../command/DeleteThingCommand.java | 32 +- .../DisconnectRelationshipCommand.java | 44 +- .../application/command/PutThingCommand.java | 31 +- .../command/RelateThingCommand.java | 79 ++ .../command/RelationshipReference.java | 22 +- .../query/ReadCollectionQuery.java | 11 +- .../application/query/ReadInstanceQuery.java | 13 +- .../query/ReadRelationshipQuery.java | 11 +- .../application/query/ThingReadQuery.java | 3 +- .../application/schema/EntityTypeRef.java | 64 ++ .../application/schema/FieldSpec.java | 32 + .../application/schema/RelationshipSpec.java | 27 + .../schema/SchemaDefinitionResolver.java | 10 + .../application/schema/SchemaViewCatalog.java | 8 + .../route/ThingRouteMapperTest.java | 4 +- .../RelationshipBodyCommandParserTest.java | 8 +- .../ThingReadRequestMapperTest.java | 9 +- .../ThingWriteRequestMapperTest.java | 16 +- .../application/ThingCommandServiceTest.java | 333 +++++++- .../application/ThingQueryServiceTest.java | 48 +- 61 files changed, 2322 insertions(+), 1102 deletions(-) create mode 100644 ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingStoreTransaction.java delete mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/BodyCreationValidator.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/AmendThingHandler.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/CreateThingHandler.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipCommandHandler.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipReferenceResolver.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/WriteTransactionRunner.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/WriteValidationPolicy.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/BodyFieldValue.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/RelateThingCommand.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/EntityTypeRef.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/FieldSpec.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/RelationshipSpec.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/SchemaDefinitionResolver.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/schema/SchemaViewCatalog.java diff --git a/checkstyle-project-fqn.xml b/checkstyle-project-fqn.xml index f72ffbff..b57ad4bd 100644 --- a/checkstyle-project-fqn.xml +++ b/checkstyle-project-fqn.xml @@ -108,6 +108,25 @@ + + + + + + + + + + + + + + + + + + + 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/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/BodyCreationValidator.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/BodyCreationValidator.java deleted file mode 100644 index 61b43b8c..00000000 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/BodyCreationValidator.java +++ /dev/null @@ -1,69 +0,0 @@ -package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; - -import java.util.List; -import java.util.Map; -import uk.co.compendiumdev.thingifier.api.http.bodyparser.ApiBodyFields; -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 ApiBodyFields bodyFields, 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 bodyFieldValues = bodyFields.asMap(); - for (String fieldName : notAllowedToCreateWithList) { - if (bodyFieldValues.containsKey(fieldName)) { - report.setValid(false); - report.addErrorMessage(String.format("Not allowed to create with %s", fieldName)); - } - } - - return report; - } - - public ValidationReport areFieldsUnique( - final ApiBodyFields bodyFields, - final EntityDefinition thingDefinition, - final EntityInstanceQuery query, - final List uniqueFields) { - - final ValidationReport report = new ValidationReport(); - - for (Map.Entry entry : bodyFields.asFlattenedStringMap()) { - - 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/adapter/http/apihandlers/DefaultThingifierApiRuntime.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/DefaultThingifierApiRuntime.java index 4353dd6f..75106d4d 100644 --- 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 @@ -17,7 +17,7 @@ public final class DefaultThingifierApiRuntime implements ThingifierApiRuntime { public DefaultThingifierApiRuntime(final Thingifier thingifier) { this.thingifier = thingifier; this.schema = new ThingifierSchemaCatalog(thingifier); - this.queryService = new ThingQueryService(); + this.queryService = new ThingQueryService(schema); } @Override @@ -42,7 +42,10 @@ public ThingifierRequestContext contextFrom(final HttpHeadersBlock requestHeader @Override public ThingCommandService commandService(final ThingifierRequestContext context) { - return new ThingCommandService(context.store()); + return new ThingCommandService( + context.store(), + schema, + thingifier.apiConfig().willApiEnforceDeclaredTypesInInput()); } @Override diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/RelationshipBodyCommandParser.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/RelationshipBodyCommandParser.java index 491ad9f9..94a87921 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/RelationshipBodyCommandParser.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/RelationshipBodyCommandParser.java @@ -4,21 +4,22 @@ import java.util.List; import java.util.Map; 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 SchemaCatalog schema) { + public RelationshipBodyCommandParser(final SchemaViewCatalog schema) { this.schema = schema; } 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(); @@ -30,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); } @@ -42,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) { @@ -69,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) { @@ -87,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)) { @@ -98,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; } } @@ -119,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; @@ -139,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; } } @@ -153,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/adapter/http/apihandlers/SchemaCatalog.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/SchemaCatalog.java index d118aae6..7ce11d4b 100644 --- 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 @@ -1,10 +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 { +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 index 53d9ca29..0218f44b 100644 --- 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 @@ -3,126 +3,65 @@ 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.apiconfig.ThingifierApiConfig; -import uk.co.compendiumdev.thingifier.application.EntityInstanceDraftBuilder; 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.PutThingCommand; -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.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.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; + private final SchemaViewCatalog schema; - public ThingBodyCommandMapper( - final SchemaCatalog schema, - final ThingifierApiConfig apiConfig, - final ThingStore store) { + public ThingBodyCommandMapper(final SchemaViewCatalog schema) { this.schema = schema; - this.apiConfig = apiConfig; - this.store = store; } public ThingWriteRequestMapping mapCreate( final ApiBodyFields bodyFields, - final EntityDefinition entity, + final EntityTypeRef entity, final boolean validateFinalRelationships) { RelationshipBodyCommands relationships = parseRelationships(bodyFields, entity); if (!relationships.validationReport().isValid()) { return invalidRelationships(relationships.validationReport()); } - ValidationReport validated = new BodyCreationValidator().validate(bodyFields, entity); - if (!validated.isValid()) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 400, - String.format( - "Invalid Creation: %s", validated.getCombinedErrorMessages()))); - } - - return mapCreateFromValidatedBody( - bodyFields, - entity, - EntityInstanceDraft.forEntity(entity), - relationships, - validateFinalRelationships); + 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 EntityDefinition entity) { - ValidationReport validated = - new BodyCreationValidator() - .areFieldsUnique( - bodyFields, - 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); - } - + final String primaryKey, final ApiBodyFields bodyFields, final EntityTypeRef entity) { RelationshipBodyCommands relationships = parseRelationships(bodyFields, entity); if (!relationships.validationReport().isValid()) { return invalidRelationships(relationships.validationReport()); } - List fieldValues = - FieldValues.fromListMapEntryStringString(bodyFields.asFlattenedStringMap()); - store.administration().accommodateProtectedIds(entity, fieldValues); - - if (apiConfig.willApiEnforceDeclaredTypesInInput()) { - ValidationReport validatedTypes = bodyFields.validateAgainstType(entity); - if (!validatedTypes.isValid()) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 400, validatedTypes.getCombinedErrorMessages())); - } - } - - try { - List ignoreFields = entity.getFieldNamesOfType(FieldType.AUTO_GUID); - EntityInstanceDraft draft = - new EntityInstanceDraftBuilder(entity) - .overrideFieldValuesFromArgsIgnoring( - FieldValues.fromListMapEntryStringString( - fieldValuesExcludingRelationships( - bodyFields, 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())); - } + return ThingWriteRequestMapping.command( + new CreateThingCommand( + entity.name(), + primaryKey, + fieldValuesExcludingRelationships(bodyFields, relationships), + bodyFieldValues(bodyFields), + relationships.references(), + true)); } public ThingWriteRequestMapping mapAmend( final ApiBodyFields bodyFields, - final EntityDefinition entity, + final EntityTypeRef entity, final String identifier, final boolean replaceExistingFieldsAndRelationships) { return mapAmend(bodyFields, entity, identifier, replaceExistingFieldsAndRelationships, ""); @@ -130,27 +69,11 @@ public ThingWriteRequestMapping mapAmend( public ThingWriteRequestMapping mapAmend( final ApiBodyFields bodyFields, - final EntityDefinition entity, + final EntityTypeRef entity, final String identifier, final boolean replaceExistingFieldsAndRelationships, final String missingInstanceMessage) { - if (apiConfig.willApiEnforceDeclaredTypesInInput()) { - List doNotValidateFields = - entity.getFieldNamesOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID); - ValidationReport validatedTypes = - bodyFields.validateAgainstTypeIgnoring(entity, doNotValidateFields); - if (!validatedTypes.isValid()) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 400, validatedTypes.getCombinedErrorMessages())); - } - } - RelationshipBodyCommands relationships = parseRelationships(bodyFields, entity); - List fieldValues = - FieldValues.fromListMapEntryStringString( - fieldValuesExcludingRelationships(bodyFields, relationships)); - if (!relationships.validationReport().isValid()) { return ThingWriteRequestMapping.error( ApiMappingError.withMessages( @@ -159,35 +82,18 @@ public ThingWriteRequestMapping mapAmend( return ThingWriteRequestMapping.command( new AmendThingCommand( - entity, + entity.name(), identifier, - fieldValues, + fieldValuesExcludingRelationships(bodyFields, relationships), + bodyFieldValues(bodyFields), replaceExistingFieldsAndRelationships, relationships.references(), missingInstanceMessage)); } public ThingWriteRequestMapping mapPut( - final ApiBodyFields bodyFields, - final EntityDefinition entity, - final String identifier) { - if (apiConfig.willApiEnforceDeclaredTypesInInput()) { - List doNotValidateFields = - entity.getFieldNamesOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID); - ValidationReport validatedTypes = - bodyFields.validateAgainstTypeIgnoring(entity, doNotValidateFields); - if (!validatedTypes.isValid()) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 400, validatedTypes.getCombinedErrorMessages())); - } - } - + final ApiBodyFields bodyFields, final EntityTypeRef entity, final String identifier) { RelationshipBodyCommands relationships = parseRelationships(bodyFields, entity); - List fieldValues = - FieldValues.fromListMapEntryStringString( - fieldValuesExcludingRelationships(bodyFields, relationships)); - if (!relationships.validationReport().isValid()) { return ThingWriteRequestMapping.error( ApiMappingError.withMessages( @@ -195,64 +101,40 @@ public ThingWriteRequestMapping mapPut( } return ThingWriteRequestMapping.command( - new PutThingCommand(entity, identifier, fieldValues, relationships.references())); + new PutThingCommand( + entity.name(), + identifier, + fieldValuesExcludingRelationships(bodyFields, relationships), + bodyFieldValues(bodyFields), + relationships.references())); } public ThingWriteRequestMapping mapCreateAndConnect( final ApiBodyFields bodyFields, - final EntityDefinition parentEntity, + final EntityTypeRef parentEntity, final String parentIdentifier, final String relationshipName, - final EntityDefinition childEntity, + final EntityTypeRef childEntity, final String routeDisplay) { - ThingWriteRequestMapping createMapping = mapCreate(bodyFields, childEntity, false); - if (createMapping.isError()) { - return createMapping; + RelationshipBodyCommands relationships = parseRelationships(bodyFields, childEntity); + if (!relationships.validationReport().isValid()) { + return invalidRelationships(relationships.validationReport()); } - CreateThingCommand create = (CreateThingCommand) createMapping.getCommand(); return ThingWriteRequestMapping.command( new CreateAndConnectRelationshipCommand( - parentEntity, + parentEntity.name(), parentIdentifier, relationshipName, - create.getDraft(), - create.getRelationships(), + childEntity.name(), + fieldValuesExcludingRelationships(bodyFields, relationships), + bodyFieldValues(bodyFields), + relationships.references(), routeDisplay)); } - private ThingWriteRequestMapping mapCreateFromValidatedBody( - final ApiBodyFields bodyFields, - final EntityDefinition entity, - final EntityInstanceDraft baseDraft, - final RelationshipBodyCommands relationships, - final boolean validateFinalRelationships) { - if (apiConfig.willApiEnforceDeclaredTypesInInput()) { - ValidationReport validatedTypes = bodyFields.validateAgainstType(entity); - if (!validatedTypes.isValid()) { - return ThingWriteRequestMapping.error( - ApiMappingError.withMessage( - 400, validatedTypes.getCombinedErrorMessages())); - } - } - - try { - List fieldValues = - FieldValues.fromListMapEntryStringString( - fieldValuesExcludingRelationships(bodyFields, relationships)); - EntityInstanceDraft draft = - new EntityInstanceDraftBuilder(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 ApiBodyFields bodyFields, final EntityDefinition entity) { + public RelationshipBodyCommands parseRelationships( + final ApiBodyFields bodyFields, final EntityTypeRef entity) { return new RelationshipBodyCommandParser(schema) .parse(bodyFields.asFlattenedStringMap(), entity); } @@ -266,25 +148,37 @@ private ThingWriteRequestMapping invalidRelationships(final ValidationReport val validation.getCombinedErrorMessages()))); } - private List> fieldValuesExcludingRelationships( + public List fieldValuesExcludingRelationships( final ApiBodyFields bodyFields, final RelationshipBodyCommands relationships) { List> flattenedArgs = new ArrayList<>(bodyFields.asFlattenedStringMap()); flattenedArgs.removeAll(relationships.relationshipEntries()); - return flattenedArgs; + return FieldValues.fromListMapEntryStringString(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()); + 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 boolean isProtectedField(final FieldType fieldType) { - return fieldType == FieldType.AUTO_INCREMENT || fieldType == FieldType.AUTO_GUID; + 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 index 5101d494..d277e9ae 100644 --- 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 @@ -10,12 +10,14 @@ import uk.co.compendiumdev.thingifier.application.command.DeleteThingCommand; import uk.co.compendiumdev.thingifier.application.command.DisconnectRelationshipCommand; import uk.co.compendiumdev.thingifier.application.command.PutThingCommand; +import uk.co.compendiumdev.thingifier.application.command.RelateThingCommand; 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; @@ -32,6 +34,11 @@ public ApiResponse map(final ThingWriteCommand command, final ThingCommandResult return errorResponseFor(command, result); } + 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); @@ -84,12 +91,20 @@ private String creationErrorMessage(final String rawMessage) { 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()); + 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:"); + } } 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 index 114709ee..f9dae96c 100644 --- 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 @@ -7,7 +7,7 @@ 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.application.schema.EntityTypeRef; import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams; public final class ThingReadRequestMapper { @@ -23,7 +23,7 @@ public ThingReadRequestMapping map( if (route instanceof CollectionRoute) { CollectionRoute collection = (CollectionRoute) route; return ThingReadRequestMapping.query( - new ReadCollectionQuery(collection.entity(), queryParams)); + new ReadCollectionQuery(collection.entity().name(), queryParams)); } if (route instanceof InstanceRoute) { @@ -35,14 +35,15 @@ public ThingReadRequestMapping map( } return ThingReadRequestMapping.query( - new ReadInstanceQuery(instance.entity(), identifierCandidate, queryParams)); + new ReadInstanceQuery( + instance.entity().name(), identifierCandidate, queryParams)); } if (route instanceof RelationshipCollectionRoute) { RelationshipCollectionRoute relationship = (RelationshipCollectionRoute) route; return ThingReadRequestMapping.query( new ReadRelationshipQuery( - relationship.parentEntity(), + relationship.parentEntity().name(), relationship.parentIdentifier(), relationship.relationshipName(), queryParams)); @@ -57,7 +58,7 @@ private ThingReadRequestMapping notFound(final String url) { 404, String.format("Could not find an instance with %s", url))); } - private EntityDefinition entityForTerm(final String term) { - return schema.definitionWithSingularOrPluralNamed(term); + private EntityTypeRef entityForTerm(final String term) { + return schema.entityWithSingularOrPluralName(term); } } 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 index 8dcc32d2..847187c0 100644 --- 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 @@ -1,6 +1,5 @@ package uk.co.compendiumdev.thingifier.adapter.http.apihandlers; -import java.util.List; 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; @@ -9,26 +8,21 @@ 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.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.repository.ThingStore; +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 SchemaCatalog schema, - final ThingifierApiConfig apiConfig, - final ThingStore store) { - this.bodyCommandMapper = new ThingBodyCommandMapper(schema, apiConfig, store); + public ThingWriteRequestMapper(final SchemaViewCatalog schema) { + this.schema = schema; + this.bodyCommandMapper = new ThingBodyCommandMapper(schema); } public ThingWriteRequestMapping mapPost( @@ -91,14 +85,14 @@ public ThingWriteRequestMapping mapDelete(final ThingRoute route) { InstanceRoute instance = (InstanceRoute) route; return ThingWriteRequestMapping.command( new DeleteThingCommand( - instance.entity(), instance.identifier(), route.originalPath())); + instance.entity().name(), instance.identifier(), route.originalPath())); } if (route instanceof RelationshipInstanceRoute) { RelationshipInstanceRoute relationship = (RelationshipInstanceRoute) route; return ThingWriteRequestMapping.command( new DisconnectRelationshipCommand( - relationship.parentEntity(), + relationship.parentEntity().name(), relationship.parentIdentifier(), relationship.relationshipName(), relationship.childIdentifier(), @@ -122,8 +116,8 @@ private ThingWriteRequestMapping mapPostToInstance( false, String.format( "No such %s entity instance with %s == %s found", - route.entity().getName(), - route.entity().getPrimaryKeyField().getName(), + route.entity().name(), + route.entity().primaryKeyFieldName(), route.identifier())); } @@ -132,49 +126,46 @@ private ThingWriteRequestMapping mapPostToInstance( 404, String.format( "Entity %s does not have a primary key defined", - route.entity().getName()))); + route.entity().name()))); } private ThingWriteRequestMapping mapPostToRelationship( final RelationshipCollectionRoute route, final ApiBodyFields bodyFields) { - List possibleRelationships = - route.parentEntity().related().getRelationships(route.relationshipName()); - RelationshipVectorDefinition relationshipToUse = possibleRelationships.get(0); - EntityDefinition thingTo = relationshipToUse.getTo(); - - List childReferenceFields = - FieldValues.fromListMapEntryStringString(bodyFields.asFlattenedStringMap()); - if (bodyReferencesExistingRelatedItem(thingTo, childReferenceFields)) { - return ThingWriteRequestMapping.command( - new ConnectExistingRelationshipCommand( - route.parentEntity(), - route.parentIdentifier(), - route.relationshipName(), - childReferenceFields, - route.originalPath())); + EntityTypeRef childEntity = firstRelationshipTarget(route); + if (childEntity == null) { + return ThingWriteRequestMapping.error( + ApiMappingError.withMessage(400, "Your request was not understood")); } - return bodyCommandMapper.mapCreateAndConnect( - bodyFields, - route.parentEntity(), - route.parentIdentifier(), - route.relationshipName(), - relationshipToUse.getTo(), - route.originalPath()); + 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(), + route.originalPath())); } - private 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; + private EntityTypeRef firstRelationshipTarget(final RelationshipCollectionRoute route) { + for (RelationshipSpec relationship : route.parentEntity().relationships()) { + if (relationship.name().equals(route.relationshipName())) { + return schema.entityWithSingularOrPluralName(relationship.toEntityName()); } } - return false; + return null; } } 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 index d8545f5c..52f53f80 100644 --- 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 @@ -1,7 +1,14 @@ 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 { @@ -16,8 +23,54 @@ 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/adapter/http/apihandlers/route/CollectionRoute.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/route/CollectionRoute.java index 48f161e1..580f39f2 100644 --- 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 @@ -1,17 +1,17 @@ package uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef; public final class CollectionRoute extends ThingRoute { - private final EntityDefinition entity; + private final EntityTypeRef entity; - CollectionRoute(final String originalPath, final EntityDefinition entity) { + CollectionRoute(final String originalPath, final EntityTypeRef entity) { super(originalPath); this.entity = entity; } - public EntityDefinition 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 index b6587371..38c21325 100644 --- 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 @@ -1,20 +1,19 @@ package uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef; public final class InstanceRoute extends ThingRoute { - private final EntityDefinition entity; + private final EntityTypeRef entity; private final String identifier; - InstanceRoute( - final String originalPath, final EntityDefinition entity, final String identifier) { + InstanceRoute(final String originalPath, final EntityTypeRef entity, final String identifier) { super(originalPath); this.entity = entity; this.identifier = identifier; } - public EntityDefinition entity() { + public EntityTypeRef entity() { return entity; } 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 index c7becd0e..da8175c6 100644 --- 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 @@ -1,16 +1,16 @@ package uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef; public final class RelationshipCollectionRoute extends ThingRoute { - private final EntityDefinition parentEntity; + private final EntityTypeRef parentEntity; private final String parentIdentifier; private final String relationshipName; RelationshipCollectionRoute( final String originalPath, - final EntityDefinition parentEntity, + final EntityTypeRef parentEntity, final String parentIdentifier, final String relationshipName) { super(originalPath); @@ -19,7 +19,7 @@ public final class RelationshipCollectionRoute extends ThingRoute { this.relationshipName = relationshipName; } - public EntityDefinition parentEntity() { + public EntityTypeRef parentEntity() { return parentEntity; } 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 index e69663ab..629d7f2b 100644 --- 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 @@ -1,17 +1,17 @@ package uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef; public final class RelationshipInstanceRoute extends ThingRoute { - private final EntityDefinition parentEntity; + private final EntityTypeRef parentEntity; private final String parentIdentifier; private final String relationshipName; private final String childIdentifier; RelationshipInstanceRoute( final String originalPath, - final EntityDefinition parentEntity, + final EntityTypeRef parentEntity, final String parentIdentifier, final String relationshipName, final String childIdentifier) { @@ -22,7 +22,7 @@ public final class RelationshipInstanceRoute extends ThingRoute { this.childIdentifier = childIdentifier; } - public EntityDefinition parentEntity() { + public EntityTypeRef parentEntity() { return parentEntity; } 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 index 3319547c..a45d8a9b 100644 --- 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 @@ -1,13 +1,13 @@ package uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route; -import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.SchemaCatalog; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; +import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef; +import uk.co.compendiumdev.thingifier.application.schema.SchemaViewCatalog; public final class ThingRouteMapper { - private final SchemaCatalog schema; + private final SchemaViewCatalog schema; - public ThingRouteMapper(final SchemaCatalog schema) { + public ThingRouteMapper(final SchemaViewCatalog schema) { this.schema = schema; } @@ -17,7 +17,7 @@ public ThingRoute map(final String path) { return new UnmatchedRoute(path, parts); } - EntityDefinition entity = schema.definitionWithSingularOrPluralNamed(parts[0]); + EntityTypeRef entity = schema.entityWithSingularOrPluralName(parts[0]); if (entity == null) { return new UnmatchedRoute(path, parts); } @@ -30,7 +30,7 @@ public ThingRoute map(final String path) { return new InstanceRoute(path, entity, parts[1]); } - if (!entity.related().hasRelationship(parts[2])) { + if (!entity.hasRelationship(parts[2])) { return new UnmatchedRoute(path, parts); } 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 index 5411920f..dc7e4503 100644 --- 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 @@ -4,10 +4,16 @@ 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() { @@ -17,4 +23,8 @@ public String 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 index f1683d83..407077fd 100644 --- 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 @@ -6,10 +6,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -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.reporting.ValidationReport; public final class ApiBodyFields { @@ -66,6 +62,18 @@ public List flattenedFields() { 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<>(); @@ -104,65 +112,29 @@ private List> flattenToStringMap( return stringsInMap; } - public ValidationReport validateAgainstType(final EntityDefinition entity) { - return validateAgainstTypeIgnoring(entity, new ArrayList<>()); + 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 ""; } - public ValidationReport validateAgainstTypeIgnoring( - final EntityDefinition entity, final List doNotValidateFields) { - ValidationReport report = new ValidationReport(); - for (Map.Entry fieldValue : fields.entrySet()) { - - if (entity.hasAnyOfFieldNamesDefined(doNotValidateFields)) { - continue; - } - - Field field = entity.getField(fieldValue.getKey()); - if (field == null) { - continue; - } - - Object value = fieldValue.getValue(); - String instanceType = "Something Else"; - if (value instanceof String) { - instanceType = "STRING"; - } - if (value instanceof Boolean) { - instanceType = "BOOLEAN"; - } - if (value instanceof Integer) { - instanceType = "INTEGER"; - } - if (value instanceof Float) { - instanceType = "NUMERIC"; - } - if (value instanceof Double) { - instanceType = "NUMERIC"; - } - - String errorMessage = - String.format( - "%s should be %s but was %s", - field.getName(), field.getType(), instanceType); - - if (field.getType() == FieldType.BOOLEAN && !(value instanceof Boolean)) { - report.setValid(false); - report.addErrorMessage(errorMessage); - } - if (field.getType() == FieldType.INTEGER - || field.getType() == FieldType.AUTO_INCREMENT) { - if (!(value instanceof Double)) { - report.setValid(false); - report.addErrorMessage(errorMessage); - } else { - fieldValue.setValue(((Double) value).intValue()); - } - } - if (field.getType() == FieldType.FLOAT && !(value instanceof Double)) { - report.setValid(false); - report.addErrorMessage(errorMessage); - } + 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 report; + return "Something Else"; } } 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 e16e03aa..95e8de74 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 @@ -29,7 +29,7 @@ public ApiResponse handle(final String url, HttpHeadersBlock requestHeaders) { public ApiResponse handle(final String url, final ThingifierRequestContext context) { ThingWriteRequestMapping mapping = - new ThingWriteRequestMapper(runtime.schema(), runtime.apiConfig(), context.store()) + new ThingWriteRequestMapper(runtime.schema()) .mapDelete(new ThingRouteMapper(runtime.schema()).map(url)); ThingCommandResultApiMapper apiMapper = new ThingCommandResultApiMapper(runtime.apiConfig()); 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 ad1bd91d..61f22b30 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 @@ -40,7 +40,7 @@ public ApiResponse handle( final ApiBodyFields bodyFields, final ThingifierRequestContext context) { ThingWriteRequestMapping mapping = - new ThingWriteRequestMapper(runtime.schema(), runtime.apiConfig(), context.store()) + new ThingWriteRequestMapper(runtime.schema()) .mapPost(new ThingRouteMapper(runtime.schema()).map(url), bodyFields); ThingCommandResultApiMapper apiMapper = new ThingCommandResultApiMapper(runtime.apiConfig()); 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 faf879a6..a1e1a47a 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 @@ -40,7 +40,7 @@ public ApiResponse handle( final ApiBodyFields bodyFields, final ThingifierRequestContext context) { ThingWriteRequestMapping mapping = - new ThingWriteRequestMapper(runtime.schema(), runtime.apiConfig(), context.store()) + new ThingWriteRequestMapper(runtime.schema()) .mapPut(new ThingRouteMapper(runtime.schema()).map(url), bodyFields); ThingCommandResultApiMapper apiMapper = new ThingCommandResultApiMapper(runtime.apiConfig()); 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..8cde8122 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/AmendThingHandler.java @@ -0,0 +1,21 @@ +package uk.co.compendiumdev.thingifier.application; + +import uk.co.compendiumdev.thingifier.application.command.AmendThingCommand; +import uk.co.compendiumdev.thingifier.application.command.PutThingCommand; + +final class AmendThingHandler { + + private final ThingCommandService service; + + AmendThingHandler(final ThingCommandService service) { + this.service = service; + } + + ThingCommandResult handle(final AmendThingCommand command) { + return service.amend(command); + } + + ThingCommandResult handle(final PutThingCommand command) { + return service.put(command); + } +} 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..a9cc48c8 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/CreateThingHandler.java @@ -0,0 +1,16 @@ +package uk.co.compendiumdev.thingifier.application; + +import uk.co.compendiumdev.thingifier.application.command.CreateThingCommand; + +final class CreateThingHandler { + + private final ThingCommandService service; + + CreateThingHandler(final ThingCommandService service) { + this.service = service; + } + + ThingCommandResult handle(final CreateThingCommand command) { + return service.create(command); + } +} 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..e6a6d347 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipCommandHandler.java @@ -0,0 +1,31 @@ +package uk.co.compendiumdev.thingifier.application; + +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; + +final class RelationshipCommandHandler { + + private final ThingCommandService service; + + RelationshipCommandHandler(final ThingCommandService service) { + this.service = service; + } + + ThingCommandResult handle(final ConnectExistingRelationshipCommand command) { + return service.connectExistingRelationship(command); + } + + ThingCommandResult handle(final CreateAndConnectRelationshipCommand command) { + return service.createAndConnect(command); + } + + ThingCommandResult handle(final RelateThingCommand command) { + return service.relate(command); + } + + ThingCommandResult handle(final DisconnectRelationshipCommand command) { + return service.disconnectRelationship(command); + } +} 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/ThingCommandService.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingCommandService.java index 653520ea..88235e53 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 @@ -11,12 +11,13 @@ import uk.co.compendiumdev.thingifier.application.command.DeleteThingCommand; import uk.co.compendiumdev.thingifier.application.command.DisconnectRelationshipCommand; import uk.co.compendiumdev.thingifier.application.command.PutThingCommand; +import uk.co.compendiumdev.thingifier.application.command.RelateThingCommand; import uk.co.compendiumdev.thingifier.application.command.RelationshipReference; import uk.co.compendiumdev.thingifier.application.command.ThingWriteCommand; +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.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.field.instance.NamedValue; import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipVectorDefinition; import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; @@ -27,23 +28,43 @@ public final class ThingCommandService { private final ThingStore store; - - public ThingCommandService(final ThingStore store) { + private final SchemaDefinitionResolver schema; + private final WriteValidationPolicy validation; + private final RelationshipReferenceResolver relationshipResolver; + private final WriteTransactionRunner transactionRunner; + private final CreateThingHandler createHandler; + private final AmendThingHandler amendHandler; + 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) { this.store = store; + this.schema = schema; + this.validation = new WriteValidationPolicy(store, enforceDeclaredTypes); + this.relationshipResolver = new RelationshipReferenceResolver(store, schema); + this.transactionRunner = new WriteTransactionRunner(store); + this.createHandler = new CreateThingHandler(this); + this.amendHandler = new AmendThingHandler(this); + this.relationshipHandler = new RelationshipCommandHandler(this); } 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); + return amendHandler.handle((AmendThingCommand) command); } if (command instanceof DeleteThingCommand) { @@ -51,22 +72,23 @@ public ThingCommandResult execute(final ThingWriteCommand command) { } if (command instanceof PutThingCommand) { - return put((PutThingCommand) command); + return amendHandler.handle((PutThingCommand) command); } if (command instanceof ConnectExistingRelationshipCommand) { - ConnectExistingRelationshipCommand connect = - (ConnectExistingRelationshipCommand) command; - return connectExistingRelationship(connect); + return relationshipHandler.handle((ConnectExistingRelationshipCommand) command); } if (command instanceof CreateAndConnectRelationshipCommand) { - return createAndConnect((CreateAndConnectRelationshipCommand) command); + return relationshipHandler.handle((CreateAndConnectRelationshipCommand) command); + } + + if (command instanceof RelateThingCommand) { + return relationshipHandler.handle((RelateThingCommand) command); } if (command instanceof DisconnectRelationshipCommand) { - DisconnectRelationshipCommand disconnect = (DisconnectRelationshipCommand) command; - return disconnectRelationship(disconnect); + return relationshipHandler.handle((DisconnectRelationshipCommand) command); } return ThingCommandResult.error( @@ -75,40 +97,105 @@ public ThingCommandResult execute(final ThingWriteCommand command) { "Unsupported command %s", command.getClass().getSimpleName()))); } - public ThingCommandResult create( - final EntityInstanceDraft draft, final List relationships) { - return create(draft, relationships, true); + ThingCommandResult create(final CreateThingCommand command) { + EntityDefinition entity = 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 = + createDraft(entity, command.getRequestedPrimaryKey(), fieldValues); + return create( + draft, command.getRelationships(), command.shouldValidateFinalRelationships()); + } catch (Exception e) { + return ThingCommandResult.error(messageFrom(e)); + } } - public ThingCommandResult create( + ThingCommandResult create( final EntityInstanceDraft draft, final List relationships, final boolean validateFinalRelationships) { - EntityInstance created = null; try { - created = store.entities().create(draft); + EntityInstance 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( + ThingCommandResult amend(final AmendThingCommand command) { + EntityDefinition entity = entityNamed(command.getEntityName()); + ThingCommandResult typeValidation = + validation.validateDeclaredFieldTypesIgnoringProtected( + entity, command.getBodyFields()); + if (typeValidation != null) { + return typeValidation; + } + + EntityInstance instance = resolveInstance(entity, command.getIdentifier()); + if (instance == null) { + String message = command.getMissingInstanceMessage(); + if (message == null || message.isEmpty()) { + message = + String.format( + "Could not find any instances with %s", command.getIdentifier()); + } + return ThingCommandResult.error(ApplicationError.notFound(message)); + } + + 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(messageFrom(e)); + } + } + + ThingCommandResult amend( final EntityInstance instance, final EntityInstanceDraft draft, final boolean replaceExistingFieldsAndRelationships, final List relationships) { - EntityInstance updated = null; RelationshipSnapshot originalRelationships = RelationshipSnapshot.capture(store, instance); try { + EntityInstance updated; if (replaceExistingFieldsAndRelationships) { updated = store.entities().replace(instance, draft); originalRelationships.disconnectFrom(store, updated); @@ -119,7 +206,6 @@ public ThingCommandResult amend( ThingCommandResult relationshipResult = connectRelationshipReferences(updated, relationships, true, false); if (relationshipResult.isError()) { - rollbackAmendment(updated, instance, originalRelationships); return relationshipResult; } @@ -129,66 +215,41 @@ public ThingCommandResult amend( return ThingCommandResult.success(updated); } catch (Exception e) { - if (updated != null) { - rollbackAmendment(updated, instance, originalRelationships); - } return ThingCommandResult.error(messageFrom(e)); } } - private ThingCommandResult amend(final AmendThingCommand command) { - if (command.hasResolvedInstance()) { - return amend( - command.getInstance(), - command.getDraft(), - command.shouldReplaceExistingFieldsAndRelationships(), - command.getRelationships()); - } - - EntityInstance instance = resolveInstance(command.getEntity(), command.getIdentifier()); - if (instance == null) { - String message = command.getMissingInstanceMessage(); - if (message == null || message.isEmpty()) { - message = - String.format( - "Could not find any instances with %s", command.getIdentifier()); - } - return ThingCommandResult.error(ApplicationError.notFound(message)); + ThingCommandResult put(final PutThingCommand command) { + EntityDefinition entity = entityNamed(command.getEntityName()); + ThingCommandResult typeValidation = + validation.validateDeclaredFieldTypesIgnoringProtected( + entity, command.getBodyFields()); + if (typeValidation != null) { + return typeValidation; } - try { - EntityInstanceDraft draft = - new EntityInstanceDraftBuilder(instance) - .setFieldValuesFrom(command.getFieldValues()); - return amend( - instance, - draft, - command.shouldReplaceExistingFieldsAndRelationships(), - command.getRelationships()); - } catch (Exception e) { - return ThingCommandResult.error(messageFrom(e)); - } - } - - private ThingCommandResult put(final PutThingCommand command) { - EntityInstance instance = resolveInstance(command.getEntity(), command.getIdentifier()); + List fieldValues = + validation.normalizedFieldValues( + entity, command.getFieldValues(), command.getBodyFields()); + EntityInstance instance = resolveInstance(entity, command.getIdentifier()); if (instance != null) { try { EntityInstanceDraft draft = - new EntityInstanceDraftBuilder(instance) - .setFieldValuesFrom(command.getFieldValues()); + new EntityInstanceDraftBuilder(instance).setFieldValuesFrom(fieldValues); return amend(instance, draft, true, command.getRelationships()); } catch (Exception e) { return ThingCommandResult.error(messageFrom(e)); } } + ThingCommandResult creationAllowed = + validation.validatePutCreate(entity, command.getIdentifier(), fieldValues); + if (creationAllowed != null) { + return creationAllowed; + } + try { - ThingCommandResult creationAllowed = validatePutCreate(command); - if (creationAllowed != null) { - return creationAllowed; - } - EntityInstanceDraft draft = createDraftWithPrimaryKey(command); + EntityInstanceDraft draft = createDraft(entity, command.getIdentifier(), fieldValues); ThingCommandResult created = create(draft, command.getRelationships(), true); if (created.isError()) { return created; @@ -199,79 +260,188 @@ private ThingCommandResult put(final PutThingCommand command) { } } - private ThingCommandResult validatePutCreate(final PutThingCommand command) { - List forbiddenPutCreationFields = - command.getEntity().getFieldsOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID); - if (!forbiddenPutCreationFields.isEmpty()) { + private ThingCommandResult delete(final DeleteThingCommand command) { + EntityDefinition entity = entityNamed(command.getEntityName()); + EntityInstance instance = resolveInstance(entity, command.getIdentifier()); + if (instance == null) { return ThingCommandResult.error( - String.format( - "Cannot create %s with PUT due to Auto fields %s", - command.getEntity().getName(), fieldNames(forbiddenPutCreationFields))); + ApplicationError.notFound( + String.format( + "Could not find any instances with %s", + command.getRouteDisplay()))); } - Field primaryKey = command.getEntity().getPrimaryKeyField(); - for (NamedValue namedValue : command.getFieldValues()) { - if (namedValue.name.equals(primaryKey.getName()) - && !namedValue.value.equals(command.getIdentifier())) { - return ThingCommandResult.error( - String.format( - "Cannot create %s with PUT as key does not match body value %s != %s", - command.getEntity().getName(), - command.getIdentifier(), - namedValue.value)); - } + try { + store.entities().delete(instance); + return ThingCommandResult.success(); + } catch (Exception e) { + return ThingCommandResult.error(messageFrom(e)); } - - return null; } - private EntityInstanceDraft createDraftWithPrimaryKey(final PutThingCommand command) { - EntityInstanceDraft baseDraft = EntityInstanceDraft.forEntity(command.getEntity()); - Field primaryKeyField = command.getEntity().getPrimaryKeyField(); - if (primaryKeyField.getType() == FieldType.AUTO_INCREMENT - || primaryKeyField.getType() == FieldType.AUTO_GUID) { - baseDraft.withProtectedField(primaryKeyField.getName(), command.getIdentifier()); - } else { - baseDraft.withField(primaryKeyField.getName(), command.getIdentifier()); + ThingCommandResult connectExistingRelationship( + final ConnectExistingRelationshipCommand command) { + EntityDefinition parentEntity = entityNamed(command.getParentEntityName()); + EntityInstance parent = resolveInstance(parentEntity, command.getParentIdentifier()); + if (parent == null) { + return parentNotFound(command.getRouteDisplay()); } - List fieldValues = new ArrayList<>(command.getFieldValues()); - store.administration().accommodateProtectedIds(command.getEntity(), fieldValues); - EntityInstanceDraft draft = - new EntityInstanceDraftBuilder(command.getEntity()) - .overrideFieldValuesFromArgsIgnoring( - fieldValues, - command.getEntity().getFieldNamesOfType(FieldType.AUTO_GUID)); - copyBaseDraftValues(baseDraft, draft); - return draft; + RelatedItemResolution related = + 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 = + relationshipErrorIfInvalid( + parent, related.instance, relationshipToUse, command.getRelationshipName()); + if (relationshipError != null) { + return relationshipError; + } + + return connectRelationship(parent, relationshipToUse.getName(), related.instance, false); } - public ThingCommandResult delete(final EntityInstance instance) { + ThingCommandResult createAndConnect(final CreateAndConnectRelationshipCommand command) { + EntityDefinition parentEntity = entityNamed(command.getParentEntityName()); + EntityInstance parent = resolveInstance(parentEntity, command.getParentIdentifier()); + if (parent == null) { + return parentNotFound(command.getRouteDisplay()); + } + + EntityDefinition childEntity = 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 { - store.entities().delete(instance); - return ThingCommandResult.success(); + EntityInstanceDraft childDraft = createDraft(childEntity, "", childFieldValues); + ThingCommandResult createResult = + create(childDraft, command.getChildRelationships(), false); + if (createResult.isError()) { + return createResult; + } + + ThingCommandResult connectResult = + connectRelationship( + parent, + command.getRelationshipName(), + createResult.getInstance(), + true); + if (connectResult.isError()) { + return connectResult.withRolledBackCreatedInstance(); + } + + return ThingCommandResult.success(createResult.getInstance()); } catch (Exception e) { return ThingCommandResult.error(messageFrom(e)); } } - private ThingCommandResult delete(final DeleteThingCommand command) { - if (command.hasResolvedInstance()) { - return delete(command.getInstance()); + ThingCommandResult relate(final RelateThingCommand command) { + EntityDefinition parentEntity = entityNamed(command.getParentEntityName()); + if (parentEntity == null) { + return parentNotFound(command.getRouteDisplay()); } - EntityInstance instance = resolveInstance(command.getEntity(), command.getIdentifier()); - if (instance == null) { + RelationshipVectorDefinition vector = + firstRelationshipVector(parentEntity, command.getRelationshipName()); + if (vector == null) { return ThingCommandResult.error( - ApplicationError.notFound( - String.format( - "Could not find any instances with %s", - command.getRouteDisplay()))); + 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 = + 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 = resolveInstance(parentEntity, command.getParentIdentifier()); + if (parent == null) { + return parentNotFound(command.getRouteDisplay()); + } + + if (referencesExistingRelatedItem) { + ConnectExistingRelationshipCommand connect = + new ConnectExistingRelationshipCommand( + command.getParentEntityName(), + command.getParentIdentifier(), + command.getRelationshipName(), + bodyFieldValues, + command.getRouteDisplay()); + return connectExistingRelationship(connect); + } + + CreateAndConnectRelationshipCommand create = + new CreateAndConnectRelationshipCommand( + command.getParentEntityName(), + command.getParentIdentifier(), + command.getRelationshipName(), + vector.getTo().getName(), + bodyFieldValues, + command.getBodyFields(), + command.getBodyRelationships(), + command.getRouteDisplay()); + ThingCommandResult result = createAndConnect(create); + if (result.isSuccessful()) { + return ThingCommandResult.created(result.getInstance()); + } + return result; + } + + ThingCommandResult disconnectRelationship(final DisconnectRelationshipCommand command) { + EntityDefinition parentEntity = entityNamed(command.getParentEntityName()); + EntityInstance parent = resolveInstance(parentEntity, command.getParentIdentifier()); + if (parent == null) { + return relationshipRouteNotFound(command.getRouteDisplay()); + } + + EntityInstance child = + relatedInstanceMatchingIdentifier( + parent, command.getRelationshipName(), command.getChildIdentifier()); + if (child == null) { + return relationshipRouteNotFound(command.getRouteDisplay()); + } + + try { + store.relationships().removeBetween(parent, child, command.getRelationshipName()); + return ThingCommandResult.success(); + } catch (Exception e) { + return ThingCommandResult.error(messageFrom(e)); } - return delete(instance); } - public ThingCommandResult connectRelationship( + ThingCommandResult connectRelationship( final EntityInstance parent, final String relationshipName, final EntityInstance child, @@ -287,7 +457,7 @@ public ThingCommandResult connectRelationship( store.relationships().disconnectBetween(parent, child, relationshipName); } if (deleteChildOnRollback) { - rollbackDelete(child); + store.entities().delete(child); } return ThingCommandResult.error(validNow.getErrorMessages()); } @@ -298,116 +468,29 @@ public ThingCommandResult connectRelationship( store.relationships().disconnectBetween(parent, child, relationshipName); } if (deleteChildOnRollback) { - rollbackDelete(child); + store.entities().delete(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)); - } - } - - private ThingCommandResult disconnectRelationship(final DisconnectRelationshipCommand command) { - if (command.hasResolvedRelationship()) { - return disconnectRelationship( - command.getParent(), command.getChild(), command.getRelationshipName()); - } - - EntityInstance parent = - resolveInstance(command.getParentEntity(), command.getParentIdentifier()); - if (parent == null) { - return relationshipRouteNotFound(command.getRouteDisplay()); - } - - EntityInstance child = - relatedInstanceMatchingIdentifier( - parent, command.getRelationshipName(), command.getChildIdentifier()); - if (child == null) { - return relationshipRouteNotFound(command.getRouteDisplay()); - } - - return disconnectRelationship(parent, child, command.getRelationshipName()); - } - - public ThingCommandResult connectRelationships( - final EntityInstance instance, final List relationships) { - return connectRelationshipReferences(instance, relationships, true, false); - } - - private ThingCommandResult createAndConnect(final CreateAndConnectRelationshipCommand command) { - EntityInstance parent = command.getParent(); - if (!command.hasResolvedParent()) { - parent = resolveInstance(command.getParentEntity(), command.getParentIdentifier()); - if (parent == null) { + private 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( - ApplicationError.notFound( - String.format( - "Could not find parent thing for relationship %s", - command.getRouteDisplay()))); + "Invalid relationships: " + String.join(", ", resolution.errors())); } + return ThingCommandResult.error(resolution.errors()); } - ThingCommandResult createResult = - create(command.getChildDraft(), command.getChildRelationships(), false); - if (createResult.isError()) { - return createResult; - } - - ThingCommandResult connectResult = - connectRelationship( - parent, command.getRelationshipName(), createResult.getInstance(), true); - if (connectResult.isError()) { - return connectResult.withRolledBackCreatedInstance(); - } - - return ThingCommandResult.success(createResult.getInstance()); - } - - private ThingCommandResult connectExistingRelationship( - final ConnectExistingRelationshipCommand command) { - if (command.hasResolvedRelationship()) { - return connectRelationship( - command.getParent(), command.getRelationshipName(), command.getChild(), false); - } - - EntityInstance parent = - resolveInstance(command.getParentEntity(), command.getParentIdentifier()); - if (parent == null) { - return ThingCommandResult.error( - ApplicationError.notFound( - String.format( - "Could not find parent thing for relationship %s", - command.getRouteDisplay()))); - } - - RelatedItemResolution related = - 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 = - relationshipErrorIfInvalid( - parent, related.instance, relationshipToUse, command.getRelationshipName()); - if (relationshipError != null) { - return relationshipError; - } - - return connectRelationship(parent, relationshipToUse.getName(), related.instance, false); + return connectRelationships( + instance, resolution.relationships(), validateFinalRelationships); } private ThingCommandResult connectRelationships( @@ -443,46 +526,63 @@ private ThingCommandResult connectRelationships( } } - 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)); + private 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); } - return ThingCommandResult.error(resolution.errors); } - return connectRelationships(instance, resolution.relationships, validateFinalRelationships); - } + 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; + } - private RelationshipResolution resolveRelationshipReferences( - final EntityInstance instance, final List references) { - List relationships = new ArrayList<>(); - List errors = new ArrayList<>(); + return new EntityInstanceDraftBuilder(entity).setFieldValuesFrom(values); + } - for (RelationshipReference reference : references) { - EntityInstance related = resolveRelationshipReference(instance, reference); - if (related == null) { - errors.add(missingRelationshipReferenceMessage(reference)); - } else { - relationships.add( - new RelationshipConnection(reference.relationshipName(), related)); - } + private EntityDefinition entityNamed(final String entityName) { + if (entityName == null || entityName.isEmpty()) { + return null; } - - return new RelationshipResolution(relationships, errors); + EntityDefinition entity = schema.entityNamed(entityName); + if (entity != null) { + return entity; + } + return schema.definitionWithSingularOrPluralName(entityName); } private EntityInstance resolveInstance( final EntityDefinition entity, final String queryIdentifier) { + if (entity == null) { + return null; + } return store.entityQueries().findByQueryIdentifier(entity, queryIdentifier); } + private ThingCommandResult parentNotFound(final String routeDisplay) { + return ThingCommandResult.error( + ApplicationError.notFound( + String.format( + "Could not find parent thing for relationship %s", routeDisplay))); + } + private ThingCommandResult relationshipRouteNotFound(final String routeDisplay) { return ThingCommandResult.error( ApplicationError.notFound( @@ -570,6 +670,31 @@ private ThingCommandResult relationshipErrorIfInvalid( return null; } + private RelationshipVectorDefinition firstRelationshipVector( + final EntityDefinition entity, final String relationshipName) { + List vectors = + entity.related().getRelationships(relationshipName); + if (vectors.isEmpty()) { + return null; + } + return vectors.get(0); + } + + private 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; + } + private EntityInstance relatedInstanceMatchingIdentifier( final EntityInstance parent, final String relationshipName, final String identifier) { for (EntityInstance related : store.relationships().listRelated(parent, relationshipName)) { @@ -594,83 +719,6 @@ private boolean matchesQueryIdentifier(final EntityInstance instance, final Stri return primaryKeyValue != null && primaryKeyValue.contentEquals(identifier); } - 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) { @@ -693,20 +741,6 @@ private boolean isRelated( 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 void copyBaseDraftValues( final EntityInstanceDraft baseDraft, final EntityInstanceDraft draft) { for (NamedValue value : baseDraft.getFieldValues()) { @@ -717,38 +751,11 @@ private void copyBaseDraftValues( } } - private String fieldNames(final List fields) { - String names = ""; - for (Field field : fields) { - if (!names.isEmpty()) { - names = names + ", "; - } - names = names + field.getName(); - } - return names; - } - - private String messageFrom(final Exception exception) { + private static 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 RelatedItemResolution { private final EntityInstance instance; @@ -804,12 +811,6 @@ private void disconnectFrom(final ThingStore store, final EntityInstance instanc } } - private void restoreTo(final ThingStore store, final EntityInstance instance) { - for (RelationshipLink link : links) { - store.relationships().connect(instance, link.relationshipName, link.related); - } - } - private void deleteFormerDependentsMadeInvalidBy( final ThingStore store, final EntityInstance instance) { for (RelationshipLink link : links) { 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..82412c35 --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/WriteValidationPolicy.java @@ -0,0 +1,230 @@ +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 validatePutCreate( + final EntityDefinition entity, + final String identifier, + final List fieldValues) { + List forbiddenPutCreationFields = + entity.getFieldsOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID); + if (!forbiddenPutCreationFields.isEmpty()) { + return ThingCommandResult.error( + String.format( + "Cannot create %s with PUT due to Auto fields %s", + entity.getName(), fieldNames(forbiddenPutCreationFields))); + } + + Field primaryKey = entity.getPrimaryKeyField(); + for (NamedValue namedValue : fieldValues) { + if (namedValue.name.equals(primaryKey.getName()) + && !namedValue.value.equals(identifier)) { + return ThingCommandResult.error( + String.format( + "Cannot create %s with PUT as key does not match body value %s != %s", + 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 a3f9d47b..03358add 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,93 +3,84 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -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; public final class AmendThingCommand implements ThingWriteCommand { - private final EntityInstance instance; - private final EntityDefinition entity; + private final String entityName; private final String identifier; - private final EntityInstanceDraft draft; private final List fieldValues; + private final List bodyFields; private final boolean replaceExistingFieldsAndRelationships; private final List relationships; private final String missingInstanceMessage; 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.entity = instance.getEntity(); - this.identifier = instance.getPrimaryKeyValue(); - this.draft = draft; - this.fieldValues = Collections.emptyList(); - this.replaceExistingFieldsAndRelationships = replaceExistingFieldsAndRelationships; - this.relationships = Collections.unmodifiableList(new ArrayList<>(relationships)); - this.missingInstanceMessage = ""; + this( + entityName, + identifier, + fieldValues, + BodyFieldValue.fromNamedValues(fieldValues), + replaceExistingFieldsAndRelationships, + relationships, + ""); } public AmendThingCommand( - final EntityDefinition entity, + final String entityName, final String identifier, final List fieldValues, final boolean replaceExistingFieldsAndRelationships, - final List relationships) { + final List relationships, + final String missingInstanceMessage) { this( - entity, + entityName, identifier, fieldValues, + BodyFieldValue.fromNamedValues(fieldValues), replaceExistingFieldsAndRelationships, relationships, - ""); + missingInstanceMessage); } public AmendThingCommand( - final EntityDefinition entity, + final String entityName, final String identifier, final List fieldValues, + final List bodyFields, final boolean replaceExistingFieldsAndRelationships, final List relationships, final String missingInstanceMessage) { - this.instance = null; - this.entity = entity; + this.entityName = entityName; this.identifier = identifier; - this.draft = null; this.fieldValues = Collections.unmodifiableList(new ArrayList<>(fieldValues)); + this.bodyFields = Collections.unmodifiableList(new ArrayList<>(bodyFields)); this.replaceExistingFieldsAndRelationships = replaceExistingFieldsAndRelationships; this.relationships = Collections.unmodifiableList(new ArrayList<>(relationships)); - this.missingInstanceMessage = missingInstanceMessage; + this.missingInstanceMessage = missingInstanceMessage == null ? "" : missingInstanceMessage; } - public EntityInstance getInstance() { - return instance; - } - - public boolean hasResolvedInstance() { - return instance != null; - } - - public EntityDefinition getEntity() { - return entity; + public String getEntityName() { + return entityName; } public String getIdentifier() { return identifier; } - public EntityInstanceDraft getDraft() { - return draft; - } - public List getFieldValues() { return fieldValues; } + public List getBodyFields() { + return bodyFields; + } + public boolean shouldReplaceExistingFieldsAndRelationships() { return replaceExistingFieldsAndRelationships; } 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 7200f91b..d2f2cefc 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 @@ -3,59 +3,32 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -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; public final class ConnectExistingRelationshipCommand implements ThingWriteCommand { - private final EntityInstance parent; - private final EntityDefinition parentEntity; + private final String parentEntityName; private final String parentIdentifier; private final String relationshipName; - private final EntityInstance child; private final List childReferenceFields; private final String routeDisplay; public ConnectExistingRelationshipCommand( - final EntityInstance parent, - final String relationshipName, - final EntityInstance child) { - this.parent = parent; - this.parentEntity = parent.getEntity(); - this.parentIdentifier = parent.getPrimaryKeyValue(); - this.relationshipName = relationshipName; - this.child = child; - this.childReferenceFields = Collections.emptyList(); - this.routeDisplay = ""; - } - - public ConnectExistingRelationshipCommand( - final EntityDefinition parentEntity, + final String parentEntityName, final String parentIdentifier, final String relationshipName, final List childReferenceFields, final String routeDisplay) { - this.parent = null; - this.parentEntity = parentEntity; + this.parentEntityName = parentEntityName; this.parentIdentifier = parentIdentifier; this.relationshipName = relationshipName; - this.child = null; this.childReferenceFields = Collections.unmodifiableList(new ArrayList<>(childReferenceFields)); - this.routeDisplay = routeDisplay; + this.routeDisplay = routeDisplay == null ? "" : routeDisplay; } - public EntityInstance getParent() { - return parent; - } - - public boolean hasResolvedRelationship() { - return parent != null && child != null; - } - - public EntityDefinition getParentEntity() { - return parentEntity; + public String getParentEntityName() { + return parentEntityName; } public String getParentIdentifier() { @@ -66,10 +39,6 @@ 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 d261c9ae..2ae69ccf 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,60 +3,59 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -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 EntityDefinition parentEntity; + 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; private final String routeDisplay; public CreateAndConnectRelationshipCommand( - final EntityInstance parent, + final String parentEntityName, + final String parentIdentifier, final String relationshipName, - final EntityInstanceDraft childDraft, - final List childRelationships) { - this.parent = parent; - this.parentEntity = parent.getEntity(); - this.parentIdentifier = parent.getPrimaryKeyValue(); - this.relationshipName = relationshipName; - this.childDraft = childDraft; - this.childRelationships = Collections.unmodifiableList(new ArrayList<>(childRelationships)); - this.routeDisplay = ""; + final String childEntityName, + final List childFieldValues, + final List childRelationships, + final String routeDisplay) { + this( + parentEntityName, + parentIdentifier, + relationshipName, + childEntityName, + childFieldValues, + BodyFieldValue.fromNamedValues(childFieldValues), + childRelationships, + routeDisplay); } public CreateAndConnectRelationshipCommand( - final EntityDefinition parentEntity, + final String parentEntityName, final String parentIdentifier, final String relationshipName, - final EntityInstanceDraft childDraft, + final String childEntityName, + final List childFieldValues, + final List childBodyFields, final List childRelationships, final String routeDisplay) { - this.parent = null; - this.parentEntity = parentEntity; + 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)); - this.routeDisplay = routeDisplay; - } - - public EntityInstance getParent() { - return parent; + this.routeDisplay = routeDisplay == null ? "" : routeDisplay; } - public boolean hasResolvedParent() { - return parent != null; - } - - public EntityDefinition getParentEntity() { - return parentEntity; + public String getParentEntityName() { + return parentEntityName; } public String getParentIdentifier() { @@ -67,8 +66,16 @@ 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 3bc943c5..12ea835f 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,40 +1,20 @@ package uk.co.compendiumdev.thingifier.application.command; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance; - public final class DeleteThingCommand implements ThingWriteCommand { - private final EntityInstance instance; - private final EntityDefinition entity; + private final String entityName; private final String identifier; private final String routeDisplay; - public DeleteThingCommand(final EntityInstance instance) { - this.instance = instance; - this.entity = instance.getEntity(); - this.identifier = instance.getPrimaryKeyValue(); - this.routeDisplay = ""; - } - public DeleteThingCommand( - final EntityDefinition entity, final String identifier, final String routeDisplay) { - this.instance = null; - this.entity = entity; + final String entityName, final String identifier, final String routeDisplay) { + this.entityName = entityName; this.identifier = identifier; - this.routeDisplay = routeDisplay; - } - - public EntityInstance getInstance() { - return instance; - } - - public boolean hasResolvedInstance() { - return instance != null; + this.routeDisplay = routeDisplay == null ? "" : routeDisplay; } - 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/command/DisconnectRelationshipCommand.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/DisconnectRelationshipCommand.java index 56c1c7c7..f2e4bf8b 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,60 +1,28 @@ package uk.co.compendiumdev.thingifier.application.command; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; -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 EntityDefinition parentEntity; + private final String parentEntityName; private final String parentIdentifier; private final String relationshipName; private final String childIdentifier; private final String routeDisplay; public DisconnectRelationshipCommand( - final EntityInstance parent, - final EntityInstance child, - final String relationshipName) { - this.parent = parent; - this.child = child; - this.parentEntity = parent.getEntity(); - this.parentIdentifier = parent.getPrimaryKeyValue(); - this.relationshipName = relationshipName; - this.childIdentifier = child.getPrimaryKeyValue(); - this.routeDisplay = ""; - } - - public DisconnectRelationshipCommand( - final EntityDefinition parentEntity, + final String parentEntityName, final String parentIdentifier, final String relationshipName, final String childIdentifier, final String routeDisplay) { - this.parent = null; - this.child = null; - this.parentEntity = parentEntity; + this.parentEntityName = parentEntityName; this.parentIdentifier = parentIdentifier; this.relationshipName = relationshipName; this.childIdentifier = childIdentifier; - this.routeDisplay = routeDisplay; - } - - public EntityInstance getParent() { - return parent; - } - - public EntityInstance getChild() { - return child; - } - - public boolean hasResolvedRelationship() { - return parent != null && child != null; + this.routeDisplay = routeDisplay == null ? "" : routeDisplay; } - public EntityDefinition getParentEntity() { - return parentEntity; + public String getParentEntityName() { + return parentEntityName; } public String getParentIdentifier() { diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/PutThingCommand.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/PutThingCommand.java index 3cfc363e..1a40b43c 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/PutThingCommand.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/PutThingCommand.java @@ -3,29 +3,44 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; public final class PutThingCommand implements ThingWriteCommand { - private final EntityDefinition entity; + private final String entityName; private final String identifier; private final List fieldValues; + private final List bodyFields; private final List relationships; public PutThingCommand( - final EntityDefinition entity, + final String entityName, final String identifier, final List fieldValues, final List relationships) { - this.entity = entity; + this( + entityName, + identifier, + fieldValues, + BodyFieldValue.fromNamedValues(fieldValues), + relationships); + } + + public PutThingCommand( + 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 EntityDefinition getEntity() { - return entity; + public String getEntityName() { + return entityName; } public String getIdentifier() { @@ -36,6 +51,10 @@ 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/command/RelateThingCommand.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/RelateThingCommand.java new file mode 100644 index 00000000..475b253c --- /dev/null +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/RelateThingCommand.java @@ -0,0 +1,79 @@ +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; + private final String routeDisplay; + + public RelateThingCommand( + final String parentEntityName, + final String parentIdentifier, + final String relationshipName, + final List bodyFieldValues, + final List bodyRelationships, + final String routeDisplay) { + this( + parentEntityName, + parentIdentifier, + relationshipName, + bodyFieldValues, + BodyFieldValue.fromNamedValues(bodyFieldValues), + bodyRelationships, + routeDisplay); + } + + public RelateThingCommand( + final String parentEntityName, + final String parentIdentifier, + final String relationshipName, + final List bodyFieldValues, + final List bodyFields, + final List bodyRelationships, + final String routeDisplay) { + 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)); + this.routeDisplay = routeDisplay == null ? "" : routeDisplay; + } + + 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; + } + + public String getRouteDisplay() { + return routeDisplay; + } +} 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/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 index 1d104a77..3273c200 100644 --- 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 @@ -16,7 +16,7 @@ void mapsCollectionRoute() { ThingRoute route = mapper().map("/tasks/"); Assertions.assertTrue(route instanceof CollectionRoute); - Assertions.assertEquals("task", ((CollectionRoute) route).entity().getName()); + Assertions.assertEquals("task", ((CollectionRoute) route).entity().name()); } @Test @@ -33,7 +33,7 @@ void mapsRelationshipCollectionRouteWithoutResolvingInstances() { Assertions.assertTrue(route instanceof RelationshipCollectionRoute); RelationshipCollectionRoute relationship = (RelationshipCollectionRoute) route; - Assertions.assertEquals("project", relationship.parentEntity().getName()); + Assertions.assertEquals("project", relationship.parentEntity().name()); Assertions.assertEquals("p1", relationship.parentIdentifier()); Assertions.assertEquals("tasks", relationship.relationshipName()); } 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 bcfdd540..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 @@ -27,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 @@ -42,8 +42,10 @@ public void parsesCompressedRelationshipAsValidationAndExecutionCommand() { body.put("task-of.guid", projectInstance.getPrimaryKeyValue()); RelationshipBodyCommands commands = - new RelationshipBodyCommandParser(new ThingifierSchemaCatalog(thingifier)) - .parse(bodyFieldsFor(body).asFlattenedStringMap(), task); + new RelationshipBodyCommandParser(schema) + .parse( + bodyFieldsFor(body).asFlattenedStringMap(), + schema.entityWithSingularOrPluralName("task")); Assertions.assertTrue(commands.validationReport().isValid()); Assertions.assertEquals(1, commands.relationshipEntries().size()); 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 e2b63921..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 @@ -27,8 +27,7 @@ public void mapsCollectionRoute() { 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 @@ -62,8 +61,7 @@ public void mapsPluralEntityNames() { 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 @@ -117,8 +115,7 @@ public void normalizesLeadingAndTrailingSlashes() { 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 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 c6cd817f..a4f2aaec 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 @@ -13,12 +13,11 @@ 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.PutThingCommand; +import uk.co.compendiumdev.thingifier.application.command.RelateThingCommand; 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; @@ -103,7 +102,7 @@ public void postInstanceMapsToUnresolvedAmendCommand() { } @Test - public void mapsRelationshipPostWithExistingChildToConnectCommand() { + public void mapsRelationshipPostWithExistingChildToRelateCommand() { Thingifier thingifier = taskProjectThingifier(); EntityInstance task = createTask(thingifier, "Task"); EntityInstance project = createProject(thingifier, "Project"); @@ -118,11 +117,11 @@ public void mapsRelationshipPostWithExistingChildToConnectCommand() { 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"); @@ -136,7 +135,7 @@ public void mapsRelationshipPostWithoutChildKeyToCreateAndConnectCommand() { parserFor("title", "New task")); Assertions.assertFalse(mapping.isError()); - Assertions.assertTrue(mapping.getCommand() instanceof CreateAndConnectRelationshipCommand); + Assertions.assertTrue(mapping.getCommand() instanceof RelateThingCommand); } @Test @@ -231,10 +230,7 @@ private EntityInstance createProject(final Thingifier thingifier, final String t } private ThingWriteRequestMapper mapperFor(final Thingifier thingifier) { - return new ThingWriteRequestMapper( - new ThingifierSchemaCatalog(thingifier), - thingifier.apiConfig(), - storeFor(thingifier)); + return new ThingWriteRequestMapper(new ThingifierSchemaCatalog(thingifier)); } private ThingStore storeFor(final Thingifier thingifier) { 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..eea97189 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,23 @@ 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.PutThingCommand; +import uk.co.compendiumdev.thingifier.application.command.RelateThingCommand; import uk.co.compendiumdev.thingifier.application.command.RelationshipReference; 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 +42,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 +74,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 +102,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 +134,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 +153,100 @@ 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(409, result.statusCode()); + Assertions.assertEquals(1, store.entityQueries().count(project)); + } + + @Test + public void putCreateRejectsAutoFieldDefinitions() { + Thingifier thingifier = taskProjectThingifier(); + ThingStore store = storeFor(thingifier); + + ThingCommandResult result = + serviceFor(thingifier, store) + .execute( + new PutThingCommand( + "task", "123", fields("title", "Nope"), List.of())); + + Assertions.assertTrue(result.isError()); + Assertions.assertEquals( + List.of("Cannot create task with PUT due to Auto fields guid"), + result.getErrorMessages()); + } + + @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 +266,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 +303,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 +338,12 @@ 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(), + taskInstance.getPrimaryKeyValue())); Assertions.assertTrue(result.isSuccessful()); Assertions.assertNull( @@ -264,10 +368,14 @@ 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( @@ -295,10 +403,14 @@ 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,14 +434,16 @@ 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"), - List.of())); + task.getName(), + fields("title", "Rolled back"), + List.of(), + "")); Assertions.assertTrue(result.isError()); Assertions.assertTrue(result.rolledBackCreatedInstance()); @@ -352,16 +466,18 @@ 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")))); + "category", "guid", "missing")), + "")); Assertions.assertTrue(result.isError()); Assertions.assertEquals(taskCount, store.entityQueries().count(task)); @@ -369,6 +485,108 @@ 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 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 +633,50 @@ 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 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 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); + } } 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)); + } } From 73465cc5f37edfa1d16dd4170bd1c1050b8c4818 Mon Sep 17 00:00:00 2001 From: Alan Richardson Date: Tue, 14 Jul 2026 16:13:26 +0100 Subject: [PATCH 3/6] Make application errors HTTP agnostic --- checkstyle-project-fqn.xml | 18 ++++ .../apihandlers/ThingBodyCommandMapper.java | 4 +- .../ThingCommandResultApiMapper.java | 56 +++++++++++-- .../application/AmendThingHandler.java | 6 +- .../application/ApplicationError.java | 68 +++++++++++++-- .../application/ThingCommandResult.java | 7 -- .../application/ThingCommandService.java | 10 +-- .../application/WriteValidationPolicy.java | 14 ++-- ...gCommand.java => ReplaceThingCommand.java} | 6 +- .../ThingCommandResultApiMapperTest.java | 84 +++++++++++++++++++ .../ThingWriteRequestMapperTest.java | 6 +- .../application/ThingCommandServiceTest.java | 62 ++++++++++++-- 12 files changed, 288 insertions(+), 53 deletions(-) rename thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/{PutThingCommand.java => ReplaceThingCommand.java} (93%) create mode 100644 thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingCommandResultApiMapperTest.java diff --git a/checkstyle-project-fqn.xml b/checkstyle-project-fqn.xml index b57ad4bd..ef420185 100644 --- a/checkstyle-project-fqn.xml +++ b/checkstyle-project-fqn.xml @@ -112,6 +112,14 @@ + + + + + + + + @@ -127,6 +135,16 @@ + + + + + + + + + + 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 index 0218f44b..3491b24f 100644 --- 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 @@ -9,7 +9,7 @@ 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.PutThingCommand; +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; @@ -101,7 +101,7 @@ public ThingWriteRequestMapping mapPut( } return ThingWriteRequestMapping.command( - new PutThingCommand( + new ReplaceThingCommand( entity.name(), identifier, fieldValuesExcludingRelationships(bodyFields, relationships), 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 index d277e9ae..f4941ebc 100644 --- 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 @@ -1,7 +1,9 @@ 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; @@ -9,8 +11,8 @@ 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.PutThingCommand; 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 { @@ -48,7 +50,7 @@ public ApiResponse map(final ThingWriteCommand command, final ThingCommandResult return ApiResponse.created(null, apiConfig); } - if (command instanceof PutThingCommand) { + if (command instanceof ReplaceThingCommand) { if (result.createdInstance()) { return ApiResponse.created(result.getInstance(), apiConfig); } @@ -69,14 +71,15 @@ public ApiResponse map(final ThingWriteCommand command, final ThingCommandResult private ApiResponse errorResponseFor( final ThingWriteCommand command, final ThingCommandResult result) { + List messages = errorMessagesFor(result); + int statusCode = statusFor(result.getError()); ApiResponse response; - if (command instanceof CreateThingCommand && result.getErrorMessages().size() == 1) { + if (command instanceof CreateThingCommand && messages.size() == 1) { response = ApiResponse.error( - result.statusCode(), - creationErrorMessage(result.getCombinedErrorMessage())); + statusCode, creationErrorMessage(String.join(", ", messages))); } else { - response = ApiResponse.error(result.statusCode(), result.getErrorMessages()); + response = ApiResponse.error(statusCode, messages); } if (result.rolledBackCreatedInstance()) { @@ -85,6 +88,44 @@ private ApiResponse errorResponseFor( 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) { + 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"))); + } + + return result.getErrorMessages(); + } + private String creationErrorMessage(final String rawMessage) { String message = rawMessage; if (message == null) { @@ -105,6 +146,7 @@ private String creationErrorMessage(final String rawMessage) { private boolean isPlainValidationMessage(final String message) { return message != null && !message.startsWith(VALIDATION_PREFIX) - && !message.startsWith("ERROR:"); + && !message.startsWith("ERROR:") + && !message.startsWith("Cannot Create"); } } 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 index 8cde8122..48dba479 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/AmendThingHandler.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/AmendThingHandler.java @@ -1,7 +1,7 @@ package uk.co.compendiumdev.thingifier.application; import uk.co.compendiumdev.thingifier.application.command.AmendThingCommand; -import uk.co.compendiumdev.thingifier.application.command.PutThingCommand; +import uk.co.compendiumdev.thingifier.application.command.ReplaceThingCommand; final class AmendThingHandler { @@ -15,7 +15,7 @@ ThingCommandResult handle(final AmendThingCommand command) { return service.amend(command); } - ThingCommandResult handle(final PutThingCommand command) { - return service.put(command); + ThingCommandResult handle(final ReplaceThingCommand command) { + return service.replace(command); } } 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 index d233b132..867ed3f0 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ApplicationError.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ApplicationError.java @@ -3,7 +3,9 @@ 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 { @@ -14,15 +16,29 @@ public enum Category { UNSUPPORTED } + public enum Code { + VALIDATION_FAILED, + NOT_FOUND, + CONFLICT, + UNSUPPORTED_COMMAND, + REPLACE_CREATE_AUTO_FIELDS_NOT_ALLOWED, + REPLACE_CREATE_KEY_MISMATCH + } + private final Category category; - private final int statusCode; + private final Code code; private final List messages; + private final Map details; private ApplicationError( - final Category category, final int statusCode, final Collection messages) { + final Category category, + final Code code, + final Collection messages, + final Map details) { this.category = category; - this.statusCode = statusCode; + this.code = code; this.messages = Collections.unmodifiableList(new ArrayList<>(messages)); + this.details = Collections.unmodifiableMap(new HashMap<>(details)); } public static ApplicationError validation(final String message) { @@ -30,30 +46,64 @@ public static ApplicationError validation(final String message) { } public static ApplicationError validation(final Collection messages) { - return new ApplicationError(Category.VALIDATION, 400, 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, 404, List.of(message)); + return new ApplicationError(Category.NOT_FOUND, Code.NOT_FOUND, List.of(message), Map.of()); } public static ApplicationError conflict(final String message) { - return new ApplicationError(Category.CONFLICT, 409, List.of(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, 400, List.of(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 int statusCode() { - return statusCode; + 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/ThingCommandResult.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingCommandResult.java index afd1a212..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 @@ -90,13 +90,6 @@ public ApplicationError getError() { return error; } - public int statusCode() { - if (error == null) { - return 200; - } - return error.statusCode(); - } - 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 88235e53..f94be2e4 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 @@ -10,9 +10,9 @@ 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.PutThingCommand; 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.application.schema.SchemaDefinitionResolver; import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition; @@ -71,8 +71,8 @@ private ThingCommandResult executeInsideTransaction(final ThingWriteCommand comm return delete((DeleteThingCommand) command); } - if (command instanceof PutThingCommand) { - return amendHandler.handle((PutThingCommand) command); + if (command instanceof ReplaceThingCommand) { + return amendHandler.handle((ReplaceThingCommand) command); } if (command instanceof ConnectExistingRelationshipCommand) { @@ -219,7 +219,7 @@ ThingCommandResult amend( } } - ThingCommandResult put(final PutThingCommand command) { + ThingCommandResult replace(final ReplaceThingCommand command) { EntityDefinition entity = entityNamed(command.getEntityName()); ThingCommandResult typeValidation = validation.validateDeclaredFieldTypesIgnoringProtected( @@ -243,7 +243,7 @@ ThingCommandResult put(final PutThingCommand command) { } ThingCommandResult creationAllowed = - validation.validatePutCreate(entity, command.getIdentifier(), fieldValues); + validation.validateReplaceCreate(entity, command.getIdentifier(), fieldValues); if (creationAllowed != null) { return creationAllowed; } 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 index 82412c35..dd5bbbdd 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/WriteValidationPolicy.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/WriteValidationPolicy.java @@ -90,17 +90,16 @@ ThingCommandResult validateCreate( return null; } - ThingCommandResult validatePutCreate( + ThingCommandResult validateReplaceCreate( final EntityDefinition entity, final String identifier, final List fieldValues) { - List forbiddenPutCreationFields = + List generatedCreationFields = entity.getFieldsOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID); - if (!forbiddenPutCreationFields.isEmpty()) { + if (!generatedCreationFields.isEmpty()) { return ThingCommandResult.error( - String.format( - "Cannot create %s with PUT due to Auto fields %s", - entity.getName(), fieldNames(forbiddenPutCreationFields))); + ApplicationError.replaceCreateAutoFieldsNotAllowed( + entity.getName(), fieldNames(generatedCreationFields))); } Field primaryKey = entity.getPrimaryKeyField(); @@ -108,8 +107,7 @@ ThingCommandResult validatePutCreate( if (namedValue.name.equals(primaryKey.getName()) && !namedValue.value.equals(identifier)) { return ThingCommandResult.error( - String.format( - "Cannot create %s with PUT as key does not match body value %s != %s", + ApplicationError.replaceCreateKeyMismatch( entity.getName(), identifier, namedValue.value)); } } diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/PutThingCommand.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/ReplaceThingCommand.java similarity index 93% rename from thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/PutThingCommand.java rename to thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/ReplaceThingCommand.java index 1a40b43c..40874731 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/PutThingCommand.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/ReplaceThingCommand.java @@ -5,7 +5,7 @@ import java.util.List; import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue; -public final class PutThingCommand implements ThingWriteCommand { +public final class ReplaceThingCommand implements ThingWriteCommand { private final String entityName; private final String identifier; @@ -13,7 +13,7 @@ public final class PutThingCommand implements ThingWriteCommand { private final List bodyFields; private final List relationships; - public PutThingCommand( + public ReplaceThingCommand( final String entityName, final String identifier, final List fieldValues, @@ -26,7 +26,7 @@ public PutThingCommand( relationships); } - public PutThingCommand( + public ReplaceThingCommand( final String entityName, final String identifier, final List fieldValues, 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..8b215e52 --- /dev/null +++ b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingCommandResultApiMapperTest.java @@ -0,0 +1,84 @@ +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.ThingCommandResultApiMapper; +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.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"))); + } + + private ThingCommandResultApiMapper mapper() { + return new ThingCommandResultApiMapper(new ThingifierApiConfig("")); + } +} 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 a4f2aaec..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 @@ -16,8 +16,8 @@ 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.PutThingCommand; 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; @@ -72,7 +72,7 @@ public void mapsPutExistingInstanceToReplaceAmendCommand() { parserFor("title", "Replaced")); Assertions.assertFalse(mapping.isError()); - Assertions.assertTrue(mapping.getCommand() instanceof PutThingCommand); + Assertions.assertTrue(mapping.getCommand() instanceof ReplaceThingCommand); } @Test @@ -84,7 +84,7 @@ public void mapsPutInstanceToPutCommand() { .mapPut(routeFor(thingifier, "note/n-1"), parserFor("title", "Created")); Assertions.assertFalse(mapping.isError()); - Assertions.assertTrue(mapping.getCommand() instanceof PutThingCommand); + Assertions.assertTrue(mapping.getCommand() instanceof ReplaceThingCommand); } @Test 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 eea97189..57c9489e 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 @@ -12,9 +12,10 @@ 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.PutThingCommand; 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; @@ -175,25 +176,64 @@ public void createCommandRejectsDuplicateRequestedPrimaryKey() { true)); Assertions.assertTrue(result.isError()); - Assertions.assertEquals(409, result.statusCode()); + 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 putCreateRejectsAutoFieldDefinitions() { + public void replaceCreateRejectsAutoFieldDefinitions() { Thingifier thingifier = taskProjectThingifier(); ThingStore store = storeFor(thingifier); ThingCommandResult result = serviceFor(thingifier, store) .execute( - new PutThingCommand( + new ReplaceThingCommand( "task", "123", fields("title", "Nope"), List.of())); Assertions.assertTrue(result.isError()); Assertions.assertEquals( - List.of("Cannot create task with PUT due to Auto fields guid"), - result.getErrorMessages()); + 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 @@ -641,6 +681,14 @@ private Thingifier typedTodoThingifier() { 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"); @@ -679,4 +727,6 @@ 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 {} } From 2bd9a049a4594355b42d850bf9cc00a915b083df Mon Sep 17 00:00:00 2001 From: Alan Richardson Date: Tue, 14 Jul 2026 16:38:22 +0100 Subject: [PATCH 4/6] Remove write route display leakage --- .../http/apihandlers/ApiRouteDisplay.java | 52 ++ .../apihandlers/ThingBodyCommandMapper.java | 18 +- .../ThingCommandResultApiMapper.java | 59 +- .../apihandlers/ThingWriteRequestMapper.java | 32 +- .../apihandlers/ThingWriteRequestMapping.java | 24 +- .../restapihandlers/RestApiDeleteHandler.java | 2 +- .../restapihandlers/RestApiPostHandler.java | 2 +- .../restapihandlers/RestApiPutHandler.java | 2 +- .../application/AmendThingHandler.java | 132 ++- .../application/ApplicationError.java | 63 ++ .../ApplicationExceptionMessages.java | 11 + .../application/CreateThingHandler.java | 79 +- .../application/DeleteThingHandler.java | 34 + .../RelationshipCommandHandler.java | 237 ++++- .../RelationshipConnectionService.java | 158 ++++ .../application/RelationshipSnapshot.java | 81 ++ .../RelationshipTargetResolver.java | 147 ++++ .../application/ThingCommandService.java | 808 +----------------- .../application/ThingDefinitionResolver.java | 35 + .../application/ThingDraftFactory.java | 60 ++ .../command/AmendThingCommand.java | 29 +- .../ConnectExistingRelationshipCommand.java | 9 +- .../CreateAndConnectRelationshipCommand.java | 15 +- .../command/DeleteThingCommand.java | 9 +- .../DisconnectRelationshipCommand.java | 9 +- .../command/RelateThingCommand.java | 15 +- .../ThingCommandResultApiMapperTest.java | 92 ++ .../application/ThingCommandServiceTest.java | 115 ++- 28 files changed, 1403 insertions(+), 926 deletions(-) create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ApiRouteDisplay.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ApplicationExceptionMessages.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/DeleteThingHandler.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipConnectionService.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipSnapshot.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipTargetResolver.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingDefinitionResolver.java create mode 100644 thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingDraftFactory.java 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/ThingBodyCommandMapper.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/adapter/http/apihandlers/ThingBodyCommandMapper.java index 3491b24f..42c28d14 100644 --- 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 @@ -64,15 +64,6 @@ public ThingWriteRequestMapping mapAmend( final EntityTypeRef entity, final String identifier, final boolean replaceExistingFieldsAndRelationships) { - return mapAmend(bodyFields, entity, identifier, replaceExistingFieldsAndRelationships, ""); - } - - public ThingWriteRequestMapping mapAmend( - final ApiBodyFields bodyFields, - final EntityTypeRef entity, - final String identifier, - final boolean replaceExistingFieldsAndRelationships, - final String missingInstanceMessage) { RelationshipBodyCommands relationships = parseRelationships(bodyFields, entity); if (!relationships.validationReport().isValid()) { return ThingWriteRequestMapping.error( @@ -87,8 +78,7 @@ public ThingWriteRequestMapping mapAmend( fieldValuesExcludingRelationships(bodyFields, relationships), bodyFieldValues(bodyFields), replaceExistingFieldsAndRelationships, - relationships.references(), - missingInstanceMessage)); + relationships.references())); } public ThingWriteRequestMapping mapPut( @@ -114,8 +104,7 @@ public ThingWriteRequestMapping mapCreateAndConnect( final EntityTypeRef parentEntity, final String parentIdentifier, final String relationshipName, - final EntityTypeRef childEntity, - final String routeDisplay) { + final EntityTypeRef childEntity) { RelationshipBodyCommands relationships = parseRelationships(bodyFields, childEntity); if (!relationships.validationReport().isValid()) { return invalidRelationships(relationships.validationReport()); @@ -129,8 +118,7 @@ public ThingWriteRequestMapping mapCreateAndConnect( childEntity.name(), fieldValuesExcludingRelationships(bodyFields, relationships), bodyFieldValues(bodyFields), - relationships.references(), - routeDisplay)); + relationships.references())); } public RelationshipBodyCommands parseRelationships( 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 index f4941ebc..3f7d4e42 100644 --- 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 @@ -31,9 +31,21 @@ 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); + return errorResponseFor(command, result, routeDisplay); } if (command instanceof RelateThingCommand) { @@ -70,8 +82,10 @@ public ApiResponse map(final ThingWriteCommand command, final ThingCommandResult } private ApiResponse errorResponseFor( - final ThingWriteCommand command, final ThingCommandResult result) { - List messages = errorMessagesFor(result); + 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) { @@ -101,7 +115,8 @@ public static int statusFor(final ApplicationError error) { return 400; } - private List errorMessagesFor(final ThingCommandResult result) { + private List errorMessagesFor( + final ThingCommandResult result, final ApiRouteDisplay routeDisplay) { ApplicationError error = result.getError(); if (error == null) { return result.getErrorMessages(); @@ -123,9 +138,45 @@ private List errorMessagesFor(final ThingCommandResult result) { 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) { 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 index 847187c0..3c5ac7cb 100644 --- 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 @@ -84,8 +84,8 @@ public ThingWriteRequestMapping mapDelete(final ThingRoute route) { if (route instanceof InstanceRoute) { InstanceRoute instance = (InstanceRoute) route; return ThingWriteRequestMapping.command( - new DeleteThingCommand( - instance.entity().name(), instance.identifier(), route.originalPath())); + new DeleteThingCommand(instance.entity().name(), instance.identifier()), + ApiRouteDisplay.originalPath(route.originalPath())); } if (route instanceof RelationshipInstanceRoute) { @@ -95,8 +95,8 @@ public ThingWriteRequestMapping mapDelete(final ThingRoute route) { relationship.parentEntity().name(), relationship.parentIdentifier(), relationship.relationshipName(), - relationship.childIdentifier(), - route.originalPath())); + relationship.childIdentifier()), + ApiRouteDisplay.originalPath(route.originalPath())); } return ThingWriteRequestMapping.error( @@ -109,16 +109,16 @@ public ThingWriteRequestMapping mapDelete(final ThingRoute route) { private ThingWriteRequestMapping mapPostToInstance( final InstanceRoute route, final ApiBodyFields bodyFields) { if (route.entity().hasPrimaryKeyField()) { - return bodyCommandMapper.mapAmend( - bodyFields, - route.entity(), - route.identifier(), - false, - String.format( - "No such %s entity instance with %s == %s found", - route.entity().name(), - route.entity().primaryKeyFieldName(), - route.identifier())); + 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( @@ -156,8 +156,8 @@ private ThingWriteRequestMapping mapPostToRelationship( bodyCommandMapper.fieldValuesExcludingRelationships( bodyFields, relationships), bodyCommandMapper.bodyFieldValues(bodyFields), - relationships.references(), - route.originalPath())); + relationships.references()), + ApiRouteDisplay.originalPath(route.originalPath())); } private EntityTypeRef firstRelationshipTarget(final RelationshipCollectionRoute route) { 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 index ccae332f..5682783b 100644 --- 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 @@ -6,18 +6,32 @@ public final class ThingWriteRequestMapping { private final ThingWriteCommand command; private final ApiMappingError error; + private final ApiRouteDisplay routeDisplay; - private ThingWriteRequestMapping(final ThingWriteCommand command, final ApiMappingError error) { + 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 new ThingWriteRequestMapping(command, null); + 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); + return new ThingWriteRequestMapping(null, error, ApiRouteDisplay.empty()); + } + + public ThingWriteRequestMapping withRouteDisplay(final ApiRouteDisplay newRouteDisplay) { + return new ThingWriteRequestMapping(command, error, newRouteDisplay); } public boolean isError() { @@ -31,4 +45,8 @@ public ThingWriteCommand getCommand() { public ApiMappingError getError() { return error; } + + public ApiRouteDisplay getRouteDisplay() { + return routeDisplay; + } } 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 95e8de74..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 @@ -38,6 +38,6 @@ public ApiResponse handle(final String url, final ThingifierRequestContext conte } ThingCommandResult result = runtime.commandService(context).execute(mapping.getCommand()); - return apiMapper.map(mapping.getCommand(), result); + return apiMapper.map(mapping, result); } } 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 61f22b30..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 @@ -49,6 +49,6 @@ public ApiResponse handle( } ThingCommandResult result = runtime.commandService(context).execute(mapping.getCommand()); - return apiMapper.map(mapping.getCommand(), result); + 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 a1e1a47a..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 @@ -49,6 +49,6 @@ public ApiResponse handle( } ThingCommandResult result = runtime.commandService(context).execute(mapping.getCommand()); - return apiMapper.map(mapping.getCommand(), result); + return apiMapper.map(mapping, result); } } 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 index 48dba479..d8cebcf0 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/AmendThingHandler.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/AmendThingHandler.java @@ -1,21 +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 ThingCommandService service; + 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 ThingCommandService service) { - this.service = service; + 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) { - return service.amend(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) { - return service.replace(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 index 867ed3f0..40beefb9 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ApplicationError.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ApplicationError.java @@ -19,6 +19,10 @@ public enum Category { 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, @@ -54,6 +58,65 @@ 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()); } 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 index a9cc48c8..164e33ad 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/CreateThingHandler.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/CreateThingHandler.java @@ -1,16 +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 ThingCommandService service; + private final ThingStore store; + private final ThingDefinitionResolver definitions; + private final WriteValidationPolicy validation; + private final ThingDraftFactory drafts; + private final RelationshipConnectionService relationships; - CreateThingHandler(final ThingCommandService service) { - this.service = service; + 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) { - return service.create(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/RelationshipCommandHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipCommandHandler.java index e6a6d347..defa454f 100644 --- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipCommandHandler.java +++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/RelationshipCommandHandler.java @@ -1,31 +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 ThingCommandService service; + 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 ThingCommandService service) { - this.service = service; + 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) { - return service.connectExistingRelationship(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) { - return service.createAndConnect(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) { - return service.relate(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) { - return service.disconnectRelationship(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/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/ThingCommandService.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/ThingCommandService.java index f94be2e4..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,9 +1,5 @@ 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; @@ -11,29 +7,17 @@ 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.application.schema.SchemaDefinitionResolver; -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.domain.instances.EntityInstanceDraft; -import uk.co.compendiumdev.thingifier.core.reporting.ValidationReport; import uk.co.compendiumdev.thingifier.core.repository.ThingStore; public final class ThingCommandService { - private final ThingStore store; - private final SchemaDefinitionResolver schema; - private final WriteValidationPolicy validation; - private final RelationshipReferenceResolver relationshipResolver; 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) { @@ -44,14 +28,36 @@ public ThingCommandService( final ThingStore store, final SchemaDefinitionResolver schema, final boolean enforceDeclaredTypes) { - this.store = store; - this.schema = schema; - this.validation = new WriteValidationPolicy(store, enforceDeclaredTypes); - this.relationshipResolver = new RelationshipReferenceResolver(store, schema); + 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(this); - this.amendHandler = new AmendThingHandler(this); - this.relationshipHandler = new RelationshipCommandHandler(this); + 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) { @@ -68,7 +74,7 @@ private ThingCommandResult executeInsideTransaction(final ThingWriteCommand comm } if (command instanceof DeleteThingCommand) { - return delete((DeleteThingCommand) command); + return deleteHandler.handle((DeleteThingCommand) command); } if (command instanceof ReplaceThingCommand) { @@ -96,756 +102,4 @@ private ThingCommandResult executeInsideTransaction(final ThingWriteCommand comm String.format( "Unsupported command %s", command.getClass().getSimpleName()))); } - - ThingCommandResult create(final CreateThingCommand command) { - EntityDefinition entity = 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 = - createDraft(entity, command.getRequestedPrimaryKey(), fieldValues); - return create( - draft, command.getRelationships(), command.shouldValidateFinalRelationships()); - } catch (Exception e) { - return ThingCommandResult.error(messageFrom(e)); - } - } - - ThingCommandResult create( - final EntityInstanceDraft draft, - final List relationships, - final boolean validateFinalRelationships) { - try { - EntityInstance created = store.entities().create(draft); - ThingCommandResult relationshipResult = - connectRelationshipReferences( - created, relationships, validateFinalRelationships, true); - if (relationshipResult.isError()) { - return relationshipResult; - } - return ThingCommandResult.success(created); - } catch (Exception e) { - return ThingCommandResult.error(messageFrom(e)); - } - } - - ThingCommandResult amend(final AmendThingCommand command) { - EntityDefinition entity = entityNamed(command.getEntityName()); - ThingCommandResult typeValidation = - validation.validateDeclaredFieldTypesIgnoringProtected( - entity, command.getBodyFields()); - if (typeValidation != null) { - return typeValidation; - } - - EntityInstance instance = resolveInstance(entity, command.getIdentifier()); - if (instance == null) { - String message = command.getMissingInstanceMessage(); - if (message == null || message.isEmpty()) { - message = - String.format( - "Could not find any instances with %s", command.getIdentifier()); - } - return ThingCommandResult.error(ApplicationError.notFound(message)); - } - - 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(messageFrom(e)); - } - } - - ThingCommandResult amend( - final EntityInstance instance, - final EntityInstanceDraft draft, - final boolean replaceExistingFieldsAndRelationships, - final List relationships) { - 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 = - connectRelationshipReferences(updated, relationships, true, false); - if (relationshipResult.isError()) { - return relationshipResult; - } - - if (replaceExistingFieldsAndRelationships) { - originalRelationships.deleteFormerDependentsMadeInvalidBy(store, updated); - } - - return ThingCommandResult.success(updated); - } catch (Exception e) { - return ThingCommandResult.error(messageFrom(e)); - } - } - - ThingCommandResult replace(final ReplaceThingCommand command) { - EntityDefinition entity = 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 = 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(messageFrom(e)); - } - } - - ThingCommandResult creationAllowed = - validation.validateReplaceCreate(entity, command.getIdentifier(), fieldValues); - if (creationAllowed != null) { - return creationAllowed; - } - - try { - EntityInstanceDraft draft = createDraft(entity, command.getIdentifier(), fieldValues); - ThingCommandResult created = create(draft, command.getRelationships(), true); - if (created.isError()) { - return created; - } - return ThingCommandResult.created(created.getInstance()); - } catch (Exception e) { - return ThingCommandResult.error(messageFrom(e)); - } - } - - private ThingCommandResult delete(final DeleteThingCommand command) { - EntityDefinition entity = entityNamed(command.getEntityName()); - EntityInstance instance = resolveInstance(entity, command.getIdentifier()); - if (instance == null) { - return ThingCommandResult.error( - ApplicationError.notFound( - String.format( - "Could not find any instances with %s", - command.getRouteDisplay()))); - } - - try { - store.entities().delete(instance); - return ThingCommandResult.success(); - } catch (Exception e) { - return ThingCommandResult.error(messageFrom(e)); - } - } - - ThingCommandResult connectExistingRelationship( - final ConnectExistingRelationshipCommand command) { - EntityDefinition parentEntity = entityNamed(command.getParentEntityName()); - EntityInstance parent = resolveInstance(parentEntity, command.getParentIdentifier()); - if (parent == null) { - return parentNotFound(command.getRouteDisplay()); - } - - RelatedItemResolution related = - 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 = - relationshipErrorIfInvalid( - parent, related.instance, relationshipToUse, command.getRelationshipName()); - if (relationshipError != null) { - return relationshipError; - } - - return connectRelationship(parent, relationshipToUse.getName(), related.instance, false); - } - - ThingCommandResult createAndConnect(final CreateAndConnectRelationshipCommand command) { - EntityDefinition parentEntity = entityNamed(command.getParentEntityName()); - EntityInstance parent = resolveInstance(parentEntity, command.getParentIdentifier()); - if (parent == null) { - return parentNotFound(command.getRouteDisplay()); - } - - EntityDefinition childEntity = 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 = createDraft(childEntity, "", childFieldValues); - ThingCommandResult createResult = - create(childDraft, command.getChildRelationships(), false); - if (createResult.isError()) { - return createResult; - } - - ThingCommandResult connectResult = - connectRelationship( - parent, - command.getRelationshipName(), - createResult.getInstance(), - true); - if (connectResult.isError()) { - return connectResult.withRolledBackCreatedInstance(); - } - - return ThingCommandResult.success(createResult.getInstance()); - } catch (Exception e) { - return ThingCommandResult.error(messageFrom(e)); - } - } - - ThingCommandResult relate(final RelateThingCommand command) { - EntityDefinition parentEntity = entityNamed(command.getParentEntityName()); - if (parentEntity == null) { - return parentNotFound(command.getRouteDisplay()); - } - - RelationshipVectorDefinition vector = - 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 = - 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 = resolveInstance(parentEntity, command.getParentIdentifier()); - if (parent == null) { - return parentNotFound(command.getRouteDisplay()); - } - - if (referencesExistingRelatedItem) { - ConnectExistingRelationshipCommand connect = - new ConnectExistingRelationshipCommand( - command.getParentEntityName(), - command.getParentIdentifier(), - command.getRelationshipName(), - bodyFieldValues, - command.getRouteDisplay()); - return connectExistingRelationship(connect); - } - - CreateAndConnectRelationshipCommand create = - new CreateAndConnectRelationshipCommand( - command.getParentEntityName(), - command.getParentIdentifier(), - command.getRelationshipName(), - vector.getTo().getName(), - bodyFieldValues, - command.getBodyFields(), - command.getBodyRelationships(), - command.getRouteDisplay()); - ThingCommandResult result = createAndConnect(create); - if (result.isSuccessful()) { - return ThingCommandResult.created(result.getInstance()); - } - return result; - } - - ThingCommandResult disconnectRelationship(final DisconnectRelationshipCommand command) { - EntityDefinition parentEntity = entityNamed(command.getParentEntityName()); - EntityInstance parent = resolveInstance(parentEntity, command.getParentIdentifier()); - if (parent == null) { - return relationshipRouteNotFound(command.getRouteDisplay()); - } - - EntityInstance child = - relatedInstanceMatchingIdentifier( - parent, command.getRelationshipName(), command.getChildIdentifier()); - if (child == null) { - return relationshipRouteNotFound(command.getRouteDisplay()); - } - - try { - store.relationships().removeBetween(parent, child, command.getRelationshipName()); - return ThingCommandResult.success(); - } catch (Exception e) { - return ThingCommandResult.error(messageFrom(e)); - } - } - - 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(messageFrom(e)); - } - } - - private 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); - } - - 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 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 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); - } - - private EntityInstance resolveInstance( - final EntityDefinition entity, final String queryIdentifier) { - if (entity == null) { - return null; - } - return store.entityQueries().findByQueryIdentifier(entity, queryIdentifier); - } - - private ThingCommandResult parentNotFound(final String routeDisplay) { - return ThingCommandResult.error( - ApplicationError.notFound( - String.format( - "Could not find parent thing for relationship %s", routeDisplay))); - } - - private ThingCommandResult relationshipRouteNotFound(final String routeDisplay) { - return ThingCommandResult.error( - ApplicationError.notFound( - String.format("Could not find any instances with %s", routeDisplay))); - } - - private 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); - } - - private 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 RelationshipVectorDefinition firstRelationshipVector( - final EntityDefinition entity, final String relationshipName) { - List vectors = - entity.related().getRelationships(relationshipName); - if (vectors.isEmpty()) { - return null; - } - return vectors.get(0); - } - - private 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; - } - - private 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); - } - - 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 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 static String messageFrom(final Exception exception) { - String message = exception.getMessage(); - return message == null ? "" : message; - } - - private 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; - } - - private static RelatedItemResolution success(final EntityInstance instance) { - return new RelatedItemResolution(instance, null); - } - - private static RelatedItemResolution error(final ApplicationError error) { - return new RelatedItemResolution(null, error); - } - } - - 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); - } - - private void disconnectFrom(final ThingStore store, final EntityInstance instance) { - for (RelationshipLink link : links) { - store.relationships() - .disconnectBetween(instance, link.related, link.relationshipName); - } - } - - 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; - } - - 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/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/command/AmendThingCommand.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/command/AmendThingCommand.java index 03358add..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 @@ -13,7 +13,6 @@ public final class AmendThingCommand implements ThingWriteCommand { private final List bodyFields; private final boolean replaceExistingFieldsAndRelationships; private final List relationships; - private final String missingInstanceMessage; public AmendThingCommand( final String entityName, @@ -27,25 +26,7 @@ public AmendThingCommand( fieldValues, BodyFieldValue.fromNamedValues(fieldValues), replaceExistingFieldsAndRelationships, - relationships, - ""); - } - - public AmendThingCommand( - final String entityName, - final String identifier, - final List fieldValues, - final boolean replaceExistingFieldsAndRelationships, - final List relationships, - final String missingInstanceMessage) { - this( - entityName, - identifier, - fieldValues, - BodyFieldValue.fromNamedValues(fieldValues), - replaceExistingFieldsAndRelationships, - relationships, - missingInstanceMessage); + relationships); } public AmendThingCommand( @@ -54,15 +35,13 @@ public AmendThingCommand( final List fieldValues, final List bodyFields, final boolean replaceExistingFieldsAndRelationships, - final List relationships, - final String missingInstanceMessage) { + 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)); - this.missingInstanceMessage = missingInstanceMessage == null ? "" : missingInstanceMessage; } public String getEntityName() { @@ -88,8 +67,4 @@ public boolean shouldReplaceExistingFieldsAndRelationships() { public List getRelationships() { return relationships; } - - public String getMissingInstanceMessage() { - return missingInstanceMessage; - } } 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 d2f2cefc..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 @@ -11,20 +11,17 @@ public final class ConnectExistingRelationshipCommand implements ThingWriteComma private final String parentIdentifier; private final String relationshipName; private final List childReferenceFields; - private final String routeDisplay; public ConnectExistingRelationshipCommand( final String parentEntityName, final String parentIdentifier, final String relationshipName, - final List childReferenceFields, - final String routeDisplay) { + final List childReferenceFields) { this.parentEntityName = parentEntityName; this.parentIdentifier = parentIdentifier; this.relationshipName = relationshipName; this.childReferenceFields = Collections.unmodifiableList(new ArrayList<>(childReferenceFields)); - this.routeDisplay = routeDisplay == null ? "" : routeDisplay; } public String getParentEntityName() { @@ -42,8 +39,4 @@ public String getRelationshipName() { public List getChildReferenceFields() { return childReferenceFields; } - - public String getRouteDisplay() { - return routeDisplay; - } } 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 2ae69ccf..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 @@ -14,7 +14,6 @@ public final class CreateAndConnectRelationshipCommand implements ThingWriteComm private final List childFieldValues; private final List childBodyFields; private final List childRelationships; - private final String routeDisplay; public CreateAndConnectRelationshipCommand( final String parentEntityName, @@ -22,8 +21,7 @@ public CreateAndConnectRelationshipCommand( final String relationshipName, final String childEntityName, final List childFieldValues, - final List childRelationships, - final String routeDisplay) { + final List childRelationships) { this( parentEntityName, parentIdentifier, @@ -31,8 +29,7 @@ public CreateAndConnectRelationshipCommand( childEntityName, childFieldValues, BodyFieldValue.fromNamedValues(childFieldValues), - childRelationships, - routeDisplay); + childRelationships); } public CreateAndConnectRelationshipCommand( @@ -42,8 +39,7 @@ public CreateAndConnectRelationshipCommand( final String childEntityName, final List childFieldValues, final List childBodyFields, - final List childRelationships, - final String routeDisplay) { + final List childRelationships) { this.parentEntityName = parentEntityName; this.parentIdentifier = parentIdentifier; this.relationshipName = relationshipName; @@ -51,7 +47,6 @@ public CreateAndConnectRelationshipCommand( this.childFieldValues = Collections.unmodifiableList(new ArrayList<>(childFieldValues)); this.childBodyFields = Collections.unmodifiableList(new ArrayList<>(childBodyFields)); this.childRelationships = Collections.unmodifiableList(new ArrayList<>(childRelationships)); - this.routeDisplay = routeDisplay == null ? "" : routeDisplay; } public String getParentEntityName() { @@ -81,8 +76,4 @@ public List getChildBodyFields() { public List getChildRelationships() { return childRelationships; } - - public String getRouteDisplay() { - return routeDisplay; - } } 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 12ea835f..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 @@ -4,13 +4,10 @@ public final class DeleteThingCommand implements ThingWriteCommand { private final String entityName; private final String identifier; - private final String routeDisplay; - public DeleteThingCommand( - final String entityName, final String identifier, final String routeDisplay) { + public DeleteThingCommand(final String entityName, final String identifier) { this.entityName = entityName; this.identifier = identifier; - this.routeDisplay = routeDisplay == null ? "" : routeDisplay; } public String getEntityName() { @@ -20,8 +17,4 @@ public String getEntityName() { public String getIdentifier() { return identifier; } - - public String getRouteDisplay() { - return routeDisplay; - } } 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 f2e4bf8b..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 @@ -6,19 +6,16 @@ public final class DisconnectRelationshipCommand implements ThingWriteCommand { private final String parentIdentifier; private final String relationshipName; private final String childIdentifier; - private final String routeDisplay; public DisconnectRelationshipCommand( final String parentEntityName, final String parentIdentifier, final String relationshipName, - final String childIdentifier, - final String routeDisplay) { + final String childIdentifier) { this.parentEntityName = parentEntityName; this.parentIdentifier = parentIdentifier; this.relationshipName = relationshipName; this.childIdentifier = childIdentifier; - this.routeDisplay = routeDisplay == null ? "" : routeDisplay; } public String getParentEntityName() { @@ -36,8 +33,4 @@ public String getRelationshipName() { public String getChildIdentifier() { return childIdentifier; } - - public String getRouteDisplay() { - return routeDisplay; - } } 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 index 475b253c..165633cc 100644 --- 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 @@ -13,23 +13,20 @@ public final class RelateThingCommand implements ThingWriteCommand { private final List bodyFieldValues; private final List bodyFields; private final List bodyRelationships; - private final String routeDisplay; public RelateThingCommand( final String parentEntityName, final String parentIdentifier, final String relationshipName, final List bodyFieldValues, - final List bodyRelationships, - final String routeDisplay) { + final List bodyRelationships) { this( parentEntityName, parentIdentifier, relationshipName, bodyFieldValues, BodyFieldValue.fromNamedValues(bodyFieldValues), - bodyRelationships, - routeDisplay); + bodyRelationships); } public RelateThingCommand( @@ -38,15 +35,13 @@ public RelateThingCommand( final String relationshipName, final List bodyFieldValues, final List bodyFields, - final List bodyRelationships, - final String routeDisplay) { + 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)); - this.routeDisplay = routeDisplay == null ? "" : routeDisplay; } public String getParentEntityName() { @@ -72,8 +67,4 @@ public List getBodyFields() { public List getBodyRelationships() { return bodyRelationships; } - - public String getRouteDisplay() { - return routeDisplay; - } } 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 index 8b215e52..86b1203a 100644 --- 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 @@ -3,12 +3,17 @@ 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 { @@ -78,6 +83,93 @@ public void mapsApplicationCategoriesToHttpStatuses() { 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/application/ThingCommandServiceTest.java b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/application/ThingCommandServiceTest.java index 57c9489e..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 @@ -381,9 +381,7 @@ public void deleteCommandDeletesInstance() { serviceFor(thingifier, store) .execute( new DeleteThingCommand( - task.getName(), - taskInstance.getPrimaryKeyValue(), - taskInstance.getPrimaryKeyValue())); + task.getName(), taskInstance.getPrimaryKeyValue())); Assertions.assertTrue(result.isSuccessful()); Assertions.assertNull( @@ -391,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(); @@ -414,8 +428,7 @@ public void disconnectRelationshipCommandRemovesOnlyRelationship() { project.getName(), projectInstance.getPrimaryKeyValue(), "tasks", - taskInstance.getPrimaryKeyValue(), - "")); + taskInstance.getPrimaryKeyValue())); Assertions.assertTrue(result.isSuccessful()); Assertions.assertTrue( @@ -425,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(); @@ -449,8 +506,7 @@ public void failedConnectExistingRelationshipDoesNotDeleteExistingChild() { project.getName(), projectInstance.getPrimaryKeyValue(), "tasks", - fields("guid", taskInstance.getPrimaryKeyValue()), - "")); + fields("guid", taskInstance.getPrimaryKeyValue()))); Assertions.assertTrue(result.isError()); Assertions.assertNotNull( @@ -482,8 +538,7 @@ public void failedCreateAndConnectRelationshipRollsBackCreatedChild() { "tasks", task.getName(), fields("title", "Rolled back"), - List.of(), - "")); + List.of())); Assertions.assertTrue(result.isError()); Assertions.assertTrue(result.rolledBackCreatedInstance()); @@ -516,8 +571,7 @@ public void failedCreateAndConnectRelationshipRollsBackOnUnresolvedChildRelation fields("title", "Rolled back"), List.of( RelationshipReference.compressed( - "category", "guid", "missing")), - "")); + "category", "guid", "missing")))); Assertions.assertTrue(result.isError()); Assertions.assertEquals(taskCount, store.entityQueries().count(task)); @@ -548,8 +602,7 @@ public void relateCommandConnectsExistingTargetWithoutCreatingChild() { projectInstance.getPrimaryKeyValue(), "tasks", fields("guid", taskInstance.getPrimaryKeyValue()), - List.of(), - "")); + List.of())); Assertions.assertTrue(result.isSuccessful()); Assertions.assertFalse(result.createdInstance()); @@ -558,6 +611,29 @@ public void relateCommandConnectsExistingTargetWithoutCreatingChild() { 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(); @@ -586,8 +662,7 @@ public void relateCommandAllowsStringAutoIncrementReferenceWhenConnectingExistin "id", taskInstance.getPrimaryKeyValue(), BodyFieldValue.SourceType.STRING)), - List.of(), - "")); + List.of())); Assertions.assertTrue(result.isSuccessful()); Assertions.assertFalse(result.createdInstance()); @@ -616,8 +691,7 @@ public void relateCommandCreatesAndConnectsWhenNoTargetReferenceIsSupplied() { projectInstance.getPrimaryKeyValue(), "tasks", fields("title", "Created task"), - List.of(), - "")); + List.of())); Assertions.assertTrue(result.isSuccessful()); Assertions.assertTrue(result.createdInstance()); @@ -709,6 +783,13 @@ 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)); } From 92bf9c2a8dd7083440812e724c53d06208653861 Mon Sep 17 00:00:00 2001 From: Alan Richardson Date: Tue, 14 Jul 2026 16:38:43 +0100 Subject: [PATCH 5/6] Move URL query parsing to HTTP edge --- .../thingifier/api/http/HttpApiRequest.java | 3 +-- .../api/http/UrlQueryParamParser.java | 12 ++++----- .../api/http/UrlQueryParamParserTest.java | 25 +++++++++---------- 3 files changed, 19 insertions(+), 21 deletions(-) rename ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/fromurl/UrlParamParser.java => thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/http/UrlQueryParamParser.java (84%) rename ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/query/urlparamsparser/FilterableParamsTest.java => thingifier/src/test/java/uk/co/compendiumdev/thingifier/api/http/UrlQueryParamParserTest.java (81%) 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/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/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()); From 07ddcea78fca5486876d97588b3f582058692ba1 Mon Sep 17 00:00:00 2001 From: Alan Richardson Date: Tue, 14 Jul 2026 16:38:58 +0100 Subject: [PATCH 6/6] Add DDD boundary guardrails --- checkstyle-project-fqn.xml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/checkstyle-project-fqn.xml b/checkstyle-project-fqn.xml index ef420185..001e0e7a 100644 --- a/checkstyle-project-fqn.xml +++ b/checkstyle-project-fqn.xml @@ -120,6 +120,14 @@ + + + + + + + + @@ -145,6 +153,26 @@ + + + + + + + + + + + + + + + + + + + +