Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
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.EntityWriteMethodConfig;
import uk.co.compendiumdev.thingifier.apiconfig.PutIdentifierPolicy;
import uk.co.compendiumdev.thingifier.application.command.DeleteThingCommand;
import uk.co.compendiumdev.thingifier.application.command.DisconnectRelationshipCommand;
import uk.co.compendiumdev.thingifier.application.command.RelateThingCommand;
Expand All @@ -19,10 +21,18 @@ public final class ThingWriteRequestMapper {

private final SchemaViewCatalog schema;
private final ThingBodyCommandMapper bodyCommandMapper;
private final EntityWriteMethodConfig entityWriteMethods;

public ThingWriteRequestMapper(final SchemaViewCatalog schema) {
this(schema, new EntityWriteMethodConfig());
}

public ThingWriteRequestMapper(
final SchemaViewCatalog schema, final EntityWriteMethodConfig entityWriteMethods) {
this.schema = schema;
this.bodyCommandMapper = new ThingBodyCommandMapper(schema);
this.entityWriteMethods =
entityWriteMethods == null ? new EntityWriteMethodConfig() : entityWriteMethods;
}

public ThingWriteRequestMapping mapPost(
Expand Down Expand Up @@ -55,13 +65,13 @@ public ThingWriteRequestMapping mapPost(

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"));
CollectionRoute collection = (CollectionRoute) route;
return mapPutToEntity(bodyFields, collection.entity(), null);
}

if (route instanceof InstanceRoute) {
InstanceRoute instance = (InstanceRoute) route;
return bodyCommandMapper.mapPut(bodyFields, instance.entity(), instance.identifier());
return mapPutToEntity(bodyFields, instance.entity(), instance.identifier());
}

if (route instanceof UnmatchedRoute) {
Expand All @@ -75,6 +85,90 @@ public ThingWriteRequestMapping mapPut(final ThingRoute route, final ApiBodyFiel
ApiMappingError.withMessage(400, "Your request was not understood"));
}

private ThingWriteRequestMapping mapPutToEntity(
final ApiBodyFields bodyFields,
final EntityTypeRef entity,
final String uriIdentifier) {
ApiMappingError identityError = putIdentifierPolicyError(bodyFields, entity, uriIdentifier);
if (identityError != null) {
return ThingWriteRequestMapping.error(identityError);
}

String identifier =
hasIdentifier(uriIdentifier)
? uriIdentifier
: payloadIdentifier(bodyFields, entity);
if (!hasIdentifier(identifier)) {
if (!entity.hasPrimaryKeyField()) {
return ThingWriteRequestMapping.error(missingPrimaryKeyDefinitionError(entity));
}
return ThingWriteRequestMapping.error(
ApiMappingError.withMessage(
422, "PUT requires an identifier in the URI or payload"));
}

return bodyCommandMapper.mapPut(bodyFields, entity, identifier);
}

private ApiMappingError putIdentifierPolicyError(
final ApiBodyFields bodyFields,
final EntityTypeRef entity,
final String uriIdentifier) {
boolean hasUriIdentifier = hasIdentifier(uriIdentifier);
if (!hasUriIdentifier
&& entityWriteMethods.putIdentifierInUri() == PutIdentifierPolicy.MANDATORY) {
return ApiMappingError.withMessage(405, "Cannot create root level entity with a PUT");
}
if (hasUriIdentifier
&& entityWriteMethods.putIdentifierInUri() == PutIdentifierPolicy.DISALLOWED) {
return ApiMappingError.withMessage(405, "Cannot identify entity with URI for PUT");
}

boolean hasPayloadIdentifier = hasPayloadIdentifier(bodyFields, entity);
if (entityWriteMethods.putIdentifierInPayload() == PutIdentifierPolicy.MANDATORY
&& !hasPayloadIdentifier) {
if (!entity.hasPrimaryKeyField()) {
return missingPrimaryKeyDefinitionError(entity);
}
return ApiMappingError.withMessage(
422,
String.format(
"PUT payload must include identifier field %s",
entity.primaryKeyFieldName()));
}
if (entityWriteMethods.putIdentifierInPayload() == PutIdentifierPolicy.DISALLOWED
&& hasPayloadIdentifier) {
return ApiMappingError.withMessage(
422,
String.format(
"PUT payload must not include identifier field %s",
entity.primaryKeyFieldName()));
}
return null;
Comment on lines +127 to +147
}

private ApiMappingError missingPrimaryKeyDefinitionError(final EntityTypeRef entity) {
return ApiMappingError.withMessage(
404, String.format("Entity %s does not have a primary key defined", entity.name()));
}

private boolean hasPayloadIdentifier(
final ApiBodyFields bodyFields, final EntityTypeRef entity) {
return entity.hasPrimaryKeyField()
&& bodyFields.asStringMap().containsKey(entity.primaryKeyFieldName());
}

private String payloadIdentifier(final ApiBodyFields bodyFields, final EntityTypeRef entity) {
if (!entity.hasPrimaryKeyField()) {
return null;
}
return bodyFields.asStringMap().get(entity.primaryKeyFieldName());
}

private boolean hasIdentifier(final String identifier) {
return identifier != null && !identifier.trim().isEmpty();
}

public ThingWriteRequestMapping mapPatch(
final ThingRoute route, final ApiBodyFields bodyFields) {
if (route instanceof CollectionRoute) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
import uk.co.compendiumdev.thingifier.api.http.ThingifierRequestContext;
import uk.co.compendiumdev.thingifier.api.http.bodyparser.ApiBodyFields;
import uk.co.compendiumdev.thingifier.api.response.ApiResponse;
import uk.co.compendiumdev.thingifier.apiconfig.ApiConfigValidationReport;
import uk.co.compendiumdev.thingifier.apiconfig.EntityPatchUpdateStyle;
import uk.co.compendiumdev.thingifier.apiconfig.EntityWriteOperation;
import uk.co.compendiumdev.thingifier.apiconfig.PutIdentifierPolicy;
import uk.co.compendiumdev.thingifier.apiconfig.RelationshipWriteOperation;
import uk.co.compendiumdev.thingifier.application.schema.EntityTypeRef;
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.field.definition.FieldType;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;

public final class WriteMethodPolicy {

Expand All @@ -34,8 +36,13 @@ public ApiResponse rejectIfNotAllowed(
final ThingRoute route,
final ApiBodyFields bodyFields,
final ThingifierRequestContext context) {
ApiResponse invalidConfig = rejectInvalidApiConfig();
if (invalidConfig != null) {
return invalidConfig;
}

if (route instanceof CollectionRoute || route instanceof InstanceRoute) {
return rejectEntityWriteIfNotAllowed(verb, route, context);
return rejectEntityWriteIfNotAllowed(verb, route, bodyFields, context);
}

if (route instanceof RelationshipCollectionRoute
Expand All @@ -49,12 +56,23 @@ public ApiResponse rejectIfNotAllowed(
private ApiResponse rejectEntityWriteIfNotAllowed(
final RoutingVerb verb,
final ThingRoute route,
final ApiBodyFields bodyFields,
final ThingifierRequestContext context) {
if (verb == RoutingVerb.PATCH) {
return rejectEntityPatchIfNotAllowed(route, context);
return rejectEntityPatchIfNotAllowed(route, bodyFields, context);
}

if (verb == RoutingVerb.PUT && !canPutRouteUseIdentifier(route)) {
return methodNotAllowed(
allowHeaderFor(
route,
bodyFields,
context,
RoutingVerb.PUT,
EntityWriteOperation.CREATE));
}

EntityWriteOperation operation = entityOperationFor(verb, route, context);
EntityWriteOperation operation = entityOperationFor(verb, route, bodyFields, context);
if (operation == null) {
return null;
}
Expand All @@ -64,19 +82,31 @@ private ApiResponse rejectEntityWriteIfNotAllowed(
return null;
}

return methodNotAllowed(allowHeaderFor(route, context, verb, operation));
return methodNotAllowed(allowHeaderFor(route, bodyFields, context, verb, operation));
}

private ApiResponse rejectEntityPatchIfNotAllowed(
final ThingRoute route, final ThingifierRequestContext context) {
final ThingRoute route,
final ApiBodyFields bodyFields,
final ThingifierRequestContext context) {
if (route instanceof CollectionRoute) {
return methodNotAllowed(
allowHeaderFor(route, context, RoutingVerb.PATCH, EntityWriteOperation.UPDATE));
allowHeaderFor(
route,
bodyFields,
context,
RoutingVerb.PATCH,
EntityWriteOperation.UPDATE));
}

if (route instanceof InstanceRoute && entityPatchUpdateStylesFor(route).isEmpty()) {
return methodNotAllowed(
allowHeaderFor(route, context, RoutingVerb.PATCH, EntityWriteOperation.UPDATE));
allowHeaderFor(
route,
bodyFields,
context,
RoutingVerb.PATCH,
EntityWriteOperation.UPDATE));
}

return null;
Expand All @@ -101,9 +131,18 @@ private ApiResponse methodNotAllowed(final String allowHeader) {
return ApiResponse.error(405, "Method Not Allowed").setHeader("Allow", allowHeader);
}

private ApiResponse rejectInvalidApiConfig() {
ApiConfigValidationReport validation = runtime.apiConfig().validate();
if (validation.isValid()) {
return null;
}
return ApiResponse.error(500, validation.errorMessages());
}

private EntityWriteOperation entityOperationFor(
final RoutingVerb verb,
final ThingRoute route,
final ApiBodyFields bodyFields,
final ThingifierRequestContext context) {
if (verb == RoutingVerb.POST && route instanceof CollectionRoute) {
return EntityWriteOperation.CREATE;
Expand All @@ -112,24 +151,69 @@ private EntityWriteOperation entityOperationFor(
&& route instanceof InstanceRoute) {
return EntityWriteOperation.UPDATE;
}
if (verb == RoutingVerb.PUT && route instanceof InstanceRoute) {
return entityInstanceExists((InstanceRoute) route, context)
if (verb == RoutingVerb.PUT
&& (route instanceof CollectionRoute || route instanceof InstanceRoute)) {
String identifier = putIdentifierFor(route, bodyFields);
if (!hasIdentifier(identifier)) {
return null;
}
return entityInstanceExists(entityFor(route), identifier, context)
? EntityWriteOperation.UPDATE
: EntityWriteOperation.CREATE;
}
return null;
}

private boolean entityInstanceExists(
final InstanceRoute route, final ThingifierRequestContext context) {
final EntityTypeRef entityRef,
final String identifier,
final ThingifierRequestContext context) {
EntityDefinition entity =
runtime.schema().definitionWithSingularOrPluralNamed(route.entity().name());
runtime.schema().definitionWithSingularOrPluralNamed(entityRef.name());
if (entity == null) {
return false;
}
EntityInstance found =
context.store().entityQueries().findByQueryIdentifier(entity, route.identifier());
return found != null;
return context.hasEntityInstanceWithIdentifier(entity, identifier);
}

private EntityTypeRef entityFor(final ThingRoute route) {
if (route instanceof CollectionRoute) {
return ((CollectionRoute) route).entity();
}
return ((InstanceRoute) route).entity();
}

private String putIdentifierFor(final ThingRoute route, final ApiBodyFields bodyFields) {
if (route instanceof InstanceRoute) {
return ((InstanceRoute) route).identifier();
}
if (route instanceof CollectionRoute) {
EntityTypeRef entity = ((CollectionRoute) route).entity();
if (entity.hasPrimaryKeyField()) {
return bodyFields.asStringMap().get(entity.primaryKeyFieldName());
}
}
return null;
}

private boolean canPutRouteUseIdentifier(final ThingRoute route) {
if (route instanceof CollectionRoute) {
CollectionRoute collection = (CollectionRoute) route;
return collection.entity().hasPrimaryKeyField()
&& runtime.apiConfig().writeMethods().entities().putIdentifierInUri()
!= PutIdentifierPolicy.MANDATORY
&& runtime.apiConfig().writeMethods().entities().putIdentifierInPayload()
!= PutIdentifierPolicy.DISALLOWED;
}
if (route instanceof InstanceRoute) {
return runtime.apiConfig().writeMethods().entities().putIdentifierInUri()
!= PutIdentifierPolicy.DISALLOWED;
}
return false;
}

private boolean hasIdentifier(final String identifier) {
return identifier != null && !identifier.trim().isEmpty();
}

private RelationshipWriteOperation relationshipOperationFor(
Expand Down Expand Up @@ -199,6 +283,7 @@ private Set<RelationshipWriteOperation> relationshipOperationsFor(

private String allowHeaderFor(
final ThingRoute route,
final ApiBodyFields bodyFields,
final ThingifierRequestContext context,
final RoutingVerb blockedVerb,
final EntityWriteOperation blockedOperation) {
Expand All @@ -211,6 +296,10 @@ private String allowHeaderFor(
.contains(EntityWriteOperation.CREATE)) {
allowed.add("POST");
}
if (isEntityMethodAllowedFor(
RoutingVerb.PUT, route, bodyFields, context, blockedVerb, blockedOperation)) {
allowed.add("PUT");
}
allowed.add("QUERY");
}
if (route instanceof InstanceRoute) {
Expand All @@ -221,7 +310,7 @@ private String allowHeaderFor(
allowed.add("POST");
}
if (isEntityMethodAllowedFor(
RoutingVerb.PUT, route, context, blockedVerb, blockedOperation)) {
RoutingVerb.PUT, route, bodyFields, context, blockedVerb, blockedOperation)) {
allowed.add("PUT");
}
if (!entityPatchUpdateStylesFor(route).isEmpty()) {
Expand All @@ -235,11 +324,20 @@ private String allowHeaderFor(
private boolean isEntityMethodAllowedFor(
final RoutingVerb verb,
final ThingRoute route,
final ApiBodyFields bodyFields,
final ThingifierRequestContext context,
final RoutingVerb blockedVerb,
final EntityWriteOperation blockedOperation) {
if (verb == RoutingVerb.PUT && !canPutRouteUseIdentifier(route)) {
return false;
}
EntityWriteOperation operation =
verb == blockedVerb ? blockedOperation : entityOperationFor(verb, route, context);
verb == blockedVerb
? blockedOperation
: entityOperationFor(verb, route, bodyFields, context);
if (operation == null && verb == RoutingVerb.PUT && route instanceof CollectionRoute) {
return !entityOperationsFor(verb, route).isEmpty();
}
return operation != null && entityOperationsFor(verb, route).contains(operation);
}

Expand Down
Loading
Loading