-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add $expand support for entity collections
This is a limited implementation of the $expand option. It has the following limitations: - Only work for read requests - Only work for attributes of the requested entity - No support for filter options based on attributes of expanded entities [1] http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions-complete.html#_Toc453752359
- Loading branch information
1 parent
d299658
commit 92dbcde
Showing
11 changed files
with
566 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,11 @@ | |
0, | ||
"always", | ||
100 | ||
], | ||
"footer-max-line-length": [ | ||
0, | ||
"always", | ||
100 | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
130 changes: 130 additions & 0 deletions
130
src/main/java/io/neonbee/internal/processor/odata/EntityExpander.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
package io.neonbee.internal.processor.odata; | ||
|
||
import static io.neonbee.entity.EntityVerticle.requestEntity; | ||
import static io.neonbee.internal.Helper.allComposite; | ||
import static io.vertx.core.Future.succeededFuture; | ||
import static java.util.stream.Collectors.toList; | ||
|
||
import java.util.ArrayList; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
import org.apache.olingo.commons.api.data.Entity; | ||
import org.apache.olingo.commons.api.data.EntityCollection; | ||
import org.apache.olingo.commons.api.data.Link; | ||
import org.apache.olingo.commons.api.edm.EdmEntityType; | ||
import org.apache.olingo.commons.api.edm.EdmNavigationProperty; | ||
import org.apache.olingo.commons.api.edm.EdmReferentialConstraint; | ||
import org.apache.olingo.commons.api.edm.constants.EdmTypeKind; | ||
import org.apache.olingo.server.api.uri.UriResourceNavigation; | ||
import org.apache.olingo.server.api.uri.queryoption.ExpandOption; | ||
|
||
import io.neonbee.data.DataQuery; | ||
import io.neonbee.data.DataRequest; | ||
import io.neonbee.data.internal.DataContextImpl; | ||
import io.vertx.core.Future; | ||
import io.vertx.core.Vertx; | ||
import io.vertx.ext.web.RoutingContext; | ||
|
||
public final class EntityExpander { | ||
private final List<EdmNavigationProperty> navigationProperties; | ||
|
||
private final Map<EdmEntityType, List<Entity>> fetchedEntities; | ||
|
||
private EntityExpander(List<EdmNavigationProperty> navigationProperties, | ||
Map<EdmEntityType, List<Entity>> fetchedEntities) { | ||
this.navigationProperties = navigationProperties; | ||
this.fetchedEntities = fetchedEntities; | ||
} | ||
|
||
/** | ||
* Creating the EntityExpander is an asynchronous operation, because during the creation the EntityExpander fetches | ||
* all referenced and <b>potentially</b> required entities based on the expand options. When the EntityExpander is | ||
* created successfully, the expand of an entity happens synchronously. | ||
* | ||
* @param vertx The Vert.x instance | ||
* @param expandOption The expand options of the OData request | ||
* @param routingContext The routingContext of the request | ||
* @return A {@link Future} holding a {@link EntityExpander} when it is completed. | ||
*/ | ||
public static Future<EntityExpander> create(Vertx vertx, ExpandOption expandOption, RoutingContext routingContext) { | ||
if (expandOption != null) { | ||
List<EdmNavigationProperty> navigationProperties = getNavigationProperties(expandOption); | ||
Map<EdmEntityType, List<Entity>> fetchedEntities = new HashMap<>(); | ||
|
||
List<Future<?>> fetchFutures = | ||
navigationProperties.stream().map(EdmNavigationProperty::getType).distinct().map(type -> { | ||
DataRequest req = new DataRequest(type.getFullQualifiedName(), new DataQuery()); | ||
return requestEntity(vertx, req, new DataContextImpl(routingContext)) | ||
.map(ew -> fetchedEntities.put(type, ew.getEntities())); | ||
}).collect(toList()); | ||
return allComposite(fetchFutures).map(v -> new EntityExpander(navigationProperties, fetchedEntities)); | ||
} else { | ||
return succeededFuture(new EntityExpander(List.of(), Map.of())); | ||
} | ||
} | ||
|
||
private static List<EdmNavigationProperty> getNavigationProperties(ExpandOption expandOption) { | ||
return expandOption.getExpandItems().stream().map(item -> item.getResourcePath().getUriResourceParts().get(0)) | ||
.filter(UriResourceNavigation.class::isInstance).map(UriResourceNavigation.class::cast) | ||
.map(UriResourceNavigation::getProperty).collect(toList()); | ||
} | ||
|
||
/** | ||
* Expands the attributes of the passed Entity. | ||
* | ||
* @param entityToExpand The entity to expand | ||
*/ | ||
public void expand(Entity entityToExpand) { | ||
for (EdmNavigationProperty navigationProperty : navigationProperties) { | ||
if (!EdmTypeKind.ENTITY.equals(navigationProperty.getType().getKind())) { | ||
throw new UnsupportedOperationException("At the moment only type Entity can be expanded"); | ||
} | ||
|
||
List<Entity> entitiesToLink = getRelatedEntities(entityToExpand, navigationProperty); | ||
linkEntities(entityToExpand, navigationProperty, entitiesToLink); | ||
} | ||
} | ||
|
||
private void linkEntities(Entity entity, EdmNavigationProperty navigationProperty, List<Entity> entitiesToLink) { | ||
Link link = new Link(); | ||
link.setTitle(navigationProperty.getName()); | ||
// Reveal if navigation property is Collection or Entity | ||
if (navigationProperty.isCollection()) { | ||
EntityCollection expandCollection = new EntityCollection(); | ||
expandCollection.getEntities().addAll(entitiesToLink); | ||
link.setInlineEntitySet(expandCollection); | ||
} else { | ||
link.setInlineEntity(entitiesToLink.get(0)); | ||
} | ||
entity.getNavigationLinks().add(link); | ||
} | ||
|
||
private List<Entity> getRelatedEntities(Entity entityToExpand, EdmNavigationProperty navigationProperty) { | ||
boolean isCollection = navigationProperty.isCollection(); | ||
|
||
List<Entity> filteredEntities = new ArrayList<>(fetchedEntities.get(navigationProperty.getType())); | ||
|
||
List<EdmReferentialConstraint> constraints = | ||
isCollection ? navigationProperty.getPartner().getReferentialConstraints() | ||
: navigationProperty.getReferentialConstraints(); | ||
|
||
for (EdmReferentialConstraint constraint : constraints) { | ||
String propertyName = isCollection ? constraint.getReferencedPropertyName() : constraint.getPropertyName(); | ||
String referencePropertyName = | ||
isCollection ? constraint.getPropertyName() : constraint.getReferencedPropertyName(); | ||
|
||
Object propertyValue = entityToExpand.getProperty(propertyName).getValue(); | ||
List<Entity> remainingEntities = List.copyOf(filteredEntities); | ||
filteredEntities.clear(); | ||
for (Entity referenceEntity : remainingEntities) { | ||
Object referencePropertyValue = referenceEntity.getProperty(referencePropertyName).getValue(); | ||
if (propertyValue.equals(referencePropertyValue)) { | ||
filteredEntities.add(referenceEntity); | ||
} | ||
} | ||
} | ||
return filteredEntities; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
96 changes: 96 additions & 0 deletions
96
src/test/java/io/neonbee/test/endpoint/odata/ODataExpandEntityCollectionTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package io.neonbee.test.endpoint.odata; | ||
|
||
import static io.neonbee.test.endpoint.odata.verticle.NavPropsCategoriesEntityVerticle.ALL_CATEGORIES; | ||
import static io.neonbee.test.endpoint.odata.verticle.NavPropsCategoriesEntityVerticle.CATEGORIES_ENTITY_SET_FQN; | ||
import static io.neonbee.test.endpoint.odata.verticle.NavPropsCategoriesEntityVerticle.FOOD_CATEGORY; | ||
import static io.neonbee.test.endpoint.odata.verticle.NavPropsCategoriesEntityVerticle.MOTORCYCLE_CATEGORY; | ||
import static io.neonbee.test.endpoint.odata.verticle.NavPropsCategoriesEntityVerticle.addProductsToCategory; | ||
import static io.neonbee.test.endpoint.odata.verticle.NavPropsProductsEntityVerticle.ALL_PRODUCTS; | ||
import static io.neonbee.test.endpoint.odata.verticle.NavPropsProductsEntityVerticle.CHEESE_PRODUCT; | ||
import static io.neonbee.test.endpoint.odata.verticle.NavPropsProductsEntityVerticle.PRODUCTS_ENTITY_SET_FQN; | ||
import static io.neonbee.test.endpoint.odata.verticle.NavPropsProductsEntityVerticle.STEAK_PRODUCT; | ||
import static io.neonbee.test.endpoint.odata.verticle.NavPropsProductsEntityVerticle.STREET_GLIDE_SPECIAL_PRODUCT; | ||
import static io.neonbee.test.endpoint.odata.verticle.NavPropsProductsEntityVerticle.S_1000_RR_PRODUCT; | ||
import static io.neonbee.test.endpoint.odata.verticle.NavPropsProductsEntityVerticle.addCategoryToProduct; | ||
import static io.neonbee.test.endpoint.odata.verticle.NavPropsProductsEntityVerticle.getDeclaredEntityModel; | ||
|
||
import java.nio.file.Path; | ||
import java.util.List; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import io.neonbee.test.base.ODataEndpointTestBase; | ||
import io.neonbee.test.base.ODataRequest; | ||
import io.neonbee.test.endpoint.odata.verticle.NavPropsCategoriesEntityVerticle; | ||
import io.neonbee.test.endpoint.odata.verticle.NavPropsProductsEntityVerticle; | ||
import io.vertx.core.CompositeFuture; | ||
import io.vertx.core.json.JsonObject; | ||
import io.vertx.junit5.Timeout; | ||
import io.vertx.junit5.VertxTestContext; | ||
|
||
public class ODataExpandEntityCollectionTest extends ODataEndpointTestBase { | ||
@Override | ||
protected List<Path> provideEntityModels() { | ||
return List.of(getDeclaredEntityModel()); | ||
} | ||
|
||
@BeforeEach | ||
void setUp(VertxTestContext testContext) { | ||
CompositeFuture | ||
.all(deployVerticle(new NavPropsProductsEntityVerticle()), | ||
deployVerticle(new NavPropsCategoriesEntityVerticle())) | ||
.onComplete(testContext.succeedingThenComplete()); | ||
} | ||
|
||
@Test | ||
@Timeout(value = 2, timeUnit = TimeUnit.SECONDS) | ||
@DisplayName("Expand property 'category' in Products") | ||
void testExpandCategoryInProducts(VertxTestContext testContext) { | ||
ODataRequest oDataRequest = new ODataRequest(PRODUCTS_ENTITY_SET_FQN).setExpandQuery("category"); | ||
List<JsonObject> expected = List.of(addCategoryToProduct(STEAK_PRODUCT, FOOD_CATEGORY), | ||
addCategoryToProduct(CHEESE_PRODUCT, FOOD_CATEGORY), | ||
addCategoryToProduct(S_1000_RR_PRODUCT, MOTORCYCLE_CATEGORY), | ||
addCategoryToProduct(STREET_GLIDE_SPECIAL_PRODUCT, MOTORCYCLE_CATEGORY)); | ||
|
||
assertODataEntitySetContainsExactly(requestOData(oDataRequest), expected, testContext) | ||
.onComplete(testContext.succeedingThenComplete()); | ||
} | ||
|
||
@Test | ||
@Timeout(value = 2, timeUnit = TimeUnit.SECONDS) | ||
@DisplayName("Do not expand property 'category' in Products") | ||
void testDoNotExpandCategoryInProducts(VertxTestContext testContext) { | ||
ODataRequest oDataRequest = new ODataRequest(PRODUCTS_ENTITY_SET_FQN); | ||
List<JsonObject> expected = ALL_PRODUCTS; | ||
|
||
assertODataEntitySetContainsExactly(requestOData(oDataRequest), expected, testContext) | ||
.onComplete(testContext.succeedingThenComplete()); | ||
} | ||
|
||
@Test | ||
@Timeout(value = 2, timeUnit = TimeUnit.HOURS) | ||
@DisplayName("Expand property 'products' in Categories") | ||
void testExpandProductsInCategory(VertxTestContext testContext) { | ||
ODataRequest oDataRequest = new ODataRequest(CATEGORIES_ENTITY_SET_FQN).setExpandQuery("products"); | ||
List<JsonObject> expected = List.of( | ||
addProductsToCategory(FOOD_CATEGORY, List.of(STEAK_PRODUCT, CHEESE_PRODUCT)), | ||
addProductsToCategory(MOTORCYCLE_CATEGORY, List.of(S_1000_RR_PRODUCT, STREET_GLIDE_SPECIAL_PRODUCT))); | ||
|
||
assertODataEntitySetContainsExactly(requestOData(oDataRequest), expected, testContext) | ||
.onComplete(testContext.succeedingThenComplete()); | ||
} | ||
|
||
@Test | ||
@Timeout(value = 2, timeUnit = TimeUnit.SECONDS) | ||
@DisplayName("Do not expand property 'products' in Categories") | ||
void testDoNotExpandProductsInCategory(VertxTestContext testContext) { | ||
ODataRequest oDataRequest = new ODataRequest(CATEGORIES_ENTITY_SET_FQN); | ||
List<JsonObject> expected = ALL_CATEGORIES; | ||
|
||
assertODataEntitySetContainsExactly(requestOData(oDataRequest), expected, testContext) | ||
.onComplete(testContext.succeedingThenComplete()); | ||
} | ||
} |
Oops, something went wrong.