Skip to content

Commit

Permalink
feat: return 200 (not 204) for PUT requests to entity verticles
Browse files Browse the repository at this point in the history
  • Loading branch information
Dapeng Wang committed Jun 2, 2023
1 parent 9f30e74 commit ec66efd
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 103 deletions.
35 changes: 35 additions & 0 deletions docs/usage/verticles/EntityVerticle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# EntityVerticle

# Create

A `POST` request will be used to create a new entity. In this case, the `Future<EntityWrapper> createData(DataQuery query, DataContext context)` method of the responsible `EntityVerticle` will be invoked.
According to the best-practices of REST-API, the response of such a POST-request should
* contain a `Location`-header with the location of the newly created entity
* return the entity representation in the response body

To achieve this, a `EntityVertile` should in the `createEntity`-method
* set the `Location` in the response context data
```
context.responseData().put("Location", /io.neonbee.test3.TestService3/TestCars(/unique-id));
```
* return a filled `EntityWrapper`
```
return Future.succeededFuture(new EntityWrapper(TEST_ENTITY_SET_FQN, createEntity(ENTITY_DATA_1));
```
In this case, a 201 status code with the entity representation will be returned in the HTTP-response.
The response context data under the key `Location` will be set as HTTP-header as well.

Just for backward-compatibility, a 204 status code will be returned, if no entity is returned in the `EntityWrapper` by the verticle.

# Update

A `PUT` request will be used to update an existing entity. In this case, the `Future<EntityWrapper> updateData(DataQuery query, DataContext context)` method of the responsible `EntityVerticle` will be invoked.
According to the best-practices of REST-API, the response of such a PUT-request should return the entity representation in the response body

To achieve this, a `EntityVertile` should return a filled `EntityWrapper` in the `updateEntity`-method
```
return Future.succeededFuture(new EntityWrapper(TEST_ENTITY_SET_FQN, createEntity(ENTITY_DATA_1));
```
In this case, a 200 status code with the entity representation will be returned in the HTTP-response.

Just for backward-compatibility, a 204 status code will be returned, if no entity is returned in the `EntityWrapper` by the verticle.
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,73 @@ public EntityProcessor(Vertx vertx, RoutingContext routingContext, Promise<Void>
super(vertx, routingContext, processPromise);
}

static Entity findEntityByKeyPredicates(RoutingContext routingContext, UriResourceEntitySet uriResourceEntitySet,
List<Entity> entities) throws ODataApplicationException {
if (entities == null || entities.isEmpty()) {
return null;
}
// Get the key predicates used to select a single entity out of the start entity set, or an empty map if
// not used. For OData services the canonical form of an absolute URI identifying a single Entity is
// formed by adding a single path segment to the service root URI. The path segment is made up of the
// name of the Service associated with the Entity followed by the key predicate identifying the Entry
// within the Entity Set (list of entities).
// The canonical key predicate for single-part keys consists only of the key property value without the
// key property name. For multi-part keys the key properties appear in the same order they appear in the
// key definition in the service metadata.
// See
// https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_CanonicalURL
// for details.
Map<String, String> keyPredicates = uriResourceEntitySet.getKeyPredicates().stream()
.collect(Collectors.toUnmodifiableMap(UriParameter::getName, UriParameter::getText));
// If key predicates were provided apply the filter to the list of entities received

// The names of the key properties provided in the key predicate query
Set<String> keyPropertyNames = keyPredicates.keySet();
List<Entity> foundEntities = entities.stream().filter(Objects::nonNull).filter(entity -> {
// Get the names of all properties of the entity
List<String> propertyNames = entity.getProperties().stream().filter(Objects::nonNull).map(Property::getName)
.collect(Collectors.toUnmodifiableList());

// Check if the entity contains all key properties
return propertyNames.containsAll(keyPropertyNames) //
// Find the entities matching all keys
&& keyPropertyNames.stream().filter(Objects::nonNull).allMatch(keyPropertyName -> { //
// Get the provided value of the key predicate
String keyPropertyValue =
EdmHelper.extractValueFromLiteral(routingContext, keyPredicates.get(keyPropertyName));

// Check if the entity's key property matches the key property from the key
// predicate by comparing the provided value with the current entity's value
try {
// Get the EdmPrimitiveTypeKind like Edm.String or Edm.Int32 of the key property
EdmPrimitiveTypeKind edmPrimitiveTypeKind =
EdmHelper.getEdmPrimitiveTypeKindByPropertyType(uriResourceEntitySet.getEntitySet()
.getEntityType().getProperty(keyPropertyName).getType().toString());

// Get the value of the key property of the current entity
Property property = entity.getProperty(keyPropertyName);
Object propertyValue = property.getValue();

// Compare the provided key property value with the entity's current value
return ENTITY_COMPARISON.comparePropertyValues(routingContext, propertyValue,
keyPropertyValue, edmPrimitiveTypeKind, keyPropertyName) == 0;
} catch (ODataApplicationException e) {
LOGGER.correlateWith(routingContext).error(e.getMessage(), e);
}
return false;
});
}).collect(Collectors.toUnmodifiableList());
if (foundEntities.size() == 1) {
return foundEntities.get(0);
} else if (foundEntities.size() > 1) {
throw new ODataApplicationException(
"Error during processing the request. More than one entity with the same ids (key properties) "
+ "was found, but ids (key properties) have to be unique.",
INTERNAL_SERVER_ERROR.getStatusCode(), Locale.ENGLISH);
}
return null;
}

@Override
public void init(OData odata, ServiceMetadata serviceMetadata) {
this.odata = odata;
Expand All @@ -113,18 +180,24 @@ public void createEntity(ODataRequest request, ODataResponse response, UriInfo u
forwardRequest(request, CREATE, entity, uriInfo, vertx, routingContext, processPromise)
.onSuccess(createdEntity -> {
try {
ContextURL contextUrl =
ContextURL.with().entitySet(uriResourceEntitySet.getEntitySet()).build();
EntitySerializerOptions opts = EntitySerializerOptions.with().contextURL(contextUrl).build();
response.setContent(odata.createSerializer(responseFormat)
.entity(serviceMetadata, uriResourceEntitySet.getEntitySet().getEntityType(),
createdEntity.getEntity(), opts)
.getContent());
response.setStatusCode(CREATED.getStatusCode());
response.setHeader(HttpHeader.CONTENT_TYPE, responseFormat.toContentTypeString());
Optional.ofNullable(
routingContext.<String>get(ProcessorHelper.RESPONSE_HEADER_PREFIX + LOCATION_HEADER))
.ifPresent(location -> response.setHeader(LOCATION_HEADER, location));
if (createdEntity.getEntity() != null) {
ContextURL contextUrl =
ContextURL.with().entitySet(uriResourceEntitySet.getEntitySet()).build();
EntitySerializerOptions opts =
EntitySerializerOptions.with().contextURL(contextUrl).build();
response.setContent(odata.createSerializer(responseFormat)
.entity(serviceMetadata, uriResourceEntitySet.getEntitySet().getEntityType(),
createdEntity.getEntity(), opts)
.getContent());
response.setStatusCode(CREATED.getStatusCode());
response.setHeader(HttpHeader.CONTENT_TYPE, responseFormat.toContentTypeString());
Optional.ofNullable(
routingContext
.<String>get(ProcessorHelper.RESPONSE_HEADER_PREFIX + LOCATION_HEADER))
.ifPresent(location -> response.setHeader(LOCATION_HEADER, location));
} else {
response.setStatusCode(NO_CONTENT.getStatusCode());
}
processPromise.complete();
} catch (SerializerException e) {
processPromise.fail(e);
Expand All @@ -140,16 +213,28 @@ public void updateEntity(ODataRequest request, ODataResponse response, UriInfo u
Entity entity = parseBody(request, uriResourceEntitySet, requestFormat);

EdmHelper.addKeyPredicateValues(entity, uriResourceEntitySet, routingContext);
forwardRequest(request, UPDATE, entity, uriInfo, vertx, routingContext, processPromise).onSuccess(ew -> {
/*
* TODO: Upon successful completion, the service responds with either 200 OK (in this case, the response
* body MUST contain the resource updated), or 204 No Content (in this case the response body is empty). The
* client may request that the response SHOULD include a body by specifying a Prefer header with a value of
* return=representation, or by specifying the system query options $select or $expand.
*/
response.setStatusCode(NO_CONTENT.getStatusCode());
processPromise.complete();
});
forwardRequest(request, UPDATE, entity, uriInfo, vertx, routingContext, processPromise)
.onSuccess(updatedEntity -> {
try {
if (updatedEntity.getEntity() != null) {
ContextURL contextUrl =
ContextURL.with().entitySet(uriResourceEntitySet.getEntitySet()).build();
EntitySerializerOptions opts =
EntitySerializerOptions.with().contextURL(contextUrl).build();
response.setContent(odata.createSerializer(responseFormat)
.entity(serviceMetadata, uriResourceEntitySet.getEntitySet().getEntityType(),
updatedEntity.getEntity(), opts)
.getContent());
response.setStatusCode(OK.getStatusCode());
response.setHeader(HttpHeader.CONTENT_TYPE, responseFormat.toContentTypeString());
} else {
response.setStatusCode(NO_CONTENT.getStatusCode());
}
processPromise.complete();
} catch (SerializerException e) {
processPromise.fail(e);
}
});
}

@Override
Expand Down Expand Up @@ -219,71 +304,4 @@ private Handler<EntityWrapper> handleReadEntityResult(UriInfo uriInfo, ODataResp
}
};
}

static Entity findEntityByKeyPredicates(RoutingContext routingContext, UriResourceEntitySet uriResourceEntitySet,
List<Entity> entities) throws ODataApplicationException {
if (entities == null || entities.isEmpty()) {
return null;
}
// Get the key predicates used to select a single entity out of the start entity set, or an empty map if
// not used. For OData services the canonical form of an absolute URI identifying a single Entity is
// formed by adding a single path segment to the service root URI. The path segment is made up of the
// name of the Service associated with the Entity followed by the key predicate identifying the Entry
// within the Entity Set (list of entities).
// The canonical key predicate for single-part keys consists only of the key property value without the
// key property name. For multi-part keys the key properties appear in the same order they appear in the
// key definition in the service metadata.
// See
// https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_CanonicalURL
// for details.
Map<String, String> keyPredicates = uriResourceEntitySet.getKeyPredicates().stream()
.collect(Collectors.toUnmodifiableMap(UriParameter::getName, UriParameter::getText));
// If key predicates were provided apply the filter to the list of entities received

// The names of the key properties provided in the key predicate query
Set<String> keyPropertyNames = keyPredicates.keySet();
List<Entity> foundEntities = entities.stream().filter(Objects::nonNull).filter(entity -> {
// Get the names of all properties of the entity
List<String> propertyNames = entity.getProperties().stream().filter(Objects::nonNull).map(Property::getName)
.collect(Collectors.toUnmodifiableList());

// Check if the entity contains all key properties
return propertyNames.containsAll(keyPropertyNames) //
// Find the entities matching all keys
&& keyPropertyNames.stream().filter(Objects::nonNull).allMatch(keyPropertyName -> { //
// Get the provided value of the key predicate
String keyPropertyValue =
EdmHelper.extractValueFromLiteral(routingContext, keyPredicates.get(keyPropertyName));

// Check if the entity's key property matches the key property from the key
// predicate by comparing the provided value with the current entity's value
try {
// Get the EdmPrimitiveTypeKind like Edm.String or Edm.Int32 of the key property
EdmPrimitiveTypeKind edmPrimitiveTypeKind =
EdmHelper.getEdmPrimitiveTypeKindByPropertyType(uriResourceEntitySet.getEntitySet()
.getEntityType().getProperty(keyPropertyName).getType().toString());

// Get the value of the key property of the current entity
Property property = entity.getProperty(keyPropertyName);
Object propertyValue = property.getValue();

// Compare the provided key property value with the entity's current value
return ENTITY_COMPARISON.comparePropertyValues(routingContext, propertyValue,
keyPropertyValue, edmPrimitiveTypeKind, keyPropertyName) == 0;
} catch (ODataApplicationException e) {
LOGGER.correlateWith(routingContext).error(e.getMessage(), e);
}
return false;
});
}).collect(Collectors.toUnmodifiableList());
if (foundEntities.size() == 1) {
return foundEntities.get(0);
} else if (foundEntities.size() > 1) {
throw new ODataApplicationException(
"Error during processing the request. More than one entity with the same ids (key properties) "
+ "was found, but ids (key properties) have to be unique.",
INTERNAL_SERVER_ERROR.getStatusCode(), Locale.ENGLISH);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ void setUp(VertxTestContext testContext) {
void createEntityTest(VertxTestContext testContext) {
ODataRequest oDataRequest = new ODataRequest(TEST_ENTITY_SET_FQN).setMethod(HttpMethod.POST)
.setBody(ENTITY_DATA_1.toBuffer())
.addHeader("expectResponseBody", "true")
.addHeader(HttpHeaders.CONTENT_TYPE.toString(), MediaType.JSON_UTF_8.toString());

requestOData(oDataRequest).onComplete(testContext.succeeding(response -> {
Expand All @@ -55,6 +56,23 @@ void createEntityTest(VertxTestContext testContext) {
}));
}

@Test
@DisplayName("Respond with 204 No Content for backward compatibility")
void createEntityTestWithNoResponse(VertxTestContext testContext) {
ODataRequest oDataRequest = new ODataRequest(TEST_ENTITY_SET_FQN).setMethod(HttpMethod.POST)
.setBody(ENTITY_DATA_1.toBuffer())
.addHeader(HttpHeaders.CONTENT_TYPE.toString(), MediaType.JSON_UTF_8.toString());

requestOData(oDataRequest).onComplete(testContext.succeeding(response -> {
testContext.verify(() -> {
assertThat(response.statusCode()).isEqualTo(204);
assertThat(response.getHeader("Location")).isNull();
assertThat(response.body()).isNull();
});
testContext.completeNow();
}));
}

@Test
@DisplayName("Respond with 400")
void createEntityTestWithWrongPayload(VertxTestContext testContext) {
Expand Down
Loading

0 comments on commit ec66efd

Please sign in to comment.