diff --git a/src/main/java/io/neonbee/endpoint/odatav4/ODataV4Endpoint.java b/src/main/java/io/neonbee/endpoint/odatav4/ODataV4Endpoint.java index 2b62f622d..a244e74f9 100644 --- a/src/main/java/io/neonbee/endpoint/odatav4/ODataV4Endpoint.java +++ b/src/main/java/io/neonbee/endpoint/odatav4/ODataV4Endpoint.java @@ -268,6 +268,16 @@ protected Future refreshRouter(Vertx vertx, Router router, String basePath models.values().stream() .filter(this::filterModels) .flatMap(entityModel -> entityModel.getAllEdmxMetadata().entrySet().stream()) + .filter(entry -> { + if (entry.getValue().getEdm().getEntityContainer() == null) { + if (LOGGER.isErrorEnabled()) { + LOGGER.error("Skipping OData service endpoint for {} — EntityContainer is null", + entry.getKey()); + } + return false; + } + return true; + }) .map(entryFunction( (schemaNamespace, edmxModel) -> Map.entry(uriConversion.apply(schemaNamespace), edmxModel))) .sorted(Map.Entry.comparingByKey(Comparator.comparingInt(String::length).reversed())) diff --git a/src/main/java/io/neonbee/entity/EntityModelLoader.java b/src/main/java/io/neonbee/entity/EntityModelLoader.java index 77231411d..b1a338df4 100644 --- a/src/main/java/io/neonbee/entity/EntityModelLoader.java +++ b/src/main/java/io/neonbee/entity/EntityModelLoader.java @@ -216,7 +216,12 @@ Future parseModel(String csnFile, byte[] csnPayload, Map a return Future.all(EntityModelDefinition.resolveEdmxPaths(Path.of(csnFile), cdsModel).stream() .map(Path::toString).map(path -> { // we do not know if the path uses windows / unix path separators, try both! - return FileSystemHelper.getPathFromMap(associatedModels, path); + byte[] payload = FileSystemHelper.getPathFromMap(associatedModels, path); + if (payload == null && LOGGER.isWarnEnabled()) { + LOGGER.warn("Associated model {} not found in associatedModels map (keys={})", path, + associatedModels.keySet()); + } + return payload; }).map(this::parseEdmxModel).toList()).onComplete(result -> { if (LOGGER.isTraceEnabled()) { LOGGER.trace("Parsing associated models of {} {}", csnFile, @@ -244,6 +249,7 @@ private void buildModelMap(CdsModel cdsModel, List edmxModels) } Map edmxMap = edmxModels.stream() + .filter(java.util.Objects::nonNull) .collect(Collectors.toMap(EntityModelLoader::getSchemaNamespace, Function.identity())); if (models.put(namespace, EntityModel.of(cdsModel, edmxMap)) != null) { LOGGER.warn("Model with schema namespace {} replaced an existing model in the model map", namespace); diff --git a/src/test/java/io/neonbee/endpoint/odatav4/ODataV4EndpointTest.java b/src/test/java/io/neonbee/endpoint/odatav4/ODataV4EndpointTest.java index 2f528d8e9..f5b0dfabb 100644 --- a/src/test/java/io/neonbee/endpoint/odatav4/ODataV4EndpointTest.java +++ b/src/test/java/io/neonbee/endpoint/odatav4/ODataV4EndpointTest.java @@ -69,7 +69,8 @@ class ODataV4EndpointTest extends ODataEndpointTestBase { @Override protected List provideEntityModels() { return List.of(TEST_RESOURCES.resolveRelated("TestService1.csn"), - TEST_RESOURCES.resolveRelated("TestService2.csn"), TEST_RESOURCES.resolveRelated("TestService3.csn")); + TEST_RESOURCES.resolveRelated("TestService2.csn"), TEST_RESOURCES.resolveRelated("TestService3.csn"), + TEST_RESOURCES.resolveRelated("EmptyContainerService.csn")); } @Override @@ -101,6 +102,23 @@ void testODataEndpointLazyLoading(VertxTestContext testContext) { .onComplete(testContext.succeedingThenComplete()); } + @Test + @DisplayName("service with empty EntityContainer is skipped, other services still registered") + void testEmptyEntityContainerServiceIsSkipped(VertxTestContext testContext) { + requestMetadata("io.neonbee.handler.empty.EmptyContainerService") + .compose(emptyResp -> { + testContext.verify(() -> assertThat(emptyResp.statusCode()).isEqualTo(HTTP_NOT_FOUND)); + return requestMetadata("io.neonbee.handler.TestService"); + }) + .onComplete(testContext.succeeding(validResp -> { + testContext.verify(() -> { + assertThat(validResp.statusCode()).isEqualTo(200); + assertThat(validResp.body().toString()).contains(" csnModelFuture = loader.readCsnModel(TEST_SERVICE_1_MODEL_PATH); - csnModelFuture.compose(v -> loader.loadModel(TEST_SERVICE_1_MODEL_PATH)) - .onComplete(testContext.succeeding(result -> testContext.verify(() -> { - CdsModel expectedCsnModel = csnModelFuture.result(); - EntityModel model = loader.models.get("io.neonbee.test1"); - assertThat(model.getCsnModel()).isEqualTo(expectedCsnModel); + loader.loadModel(TEST_SERVICE_1_MODEL_PATH) + .onComplete(testContext.succeeding(v -> testContext.verify(() -> { + var csnModel = loader.models.get("io.neonbee.test1").getCsnModel(); + assertThat(csnModel.services().map(CdsService::getQualifiedName).toList()) + .containsExactly("io.neonbee.test1.TestService1"); + assertThat(csnModel.entities().map(CdsEntity::getQualifiedName).toList()) + .containsExactly("io.neonbee.test1.TestService1.AllPropertiesNullable", + "io.neonbee.test.TestService1.TestProducts"); testContext.completeNow(); }))); } diff --git a/src/test/resources/io/neonbee/endpoint/odatav4/EmptyContainerService.csn b/src/test/resources/io/neonbee/endpoint/odatav4/EmptyContainerService.csn new file mode 100644 index 000000000..fc71accd9 --- /dev/null +++ b/src/test/resources/io/neonbee/endpoint/odatav4/EmptyContainerService.csn @@ -0,0 +1,12 @@ +{ + "namespace": "io.neonbee.handler.empty", + "definitions": { + "io.neonbee.handler.empty.EmptyContainerService": { + "kind": "service" + } + }, + "meta": { + "creator": "CDS Compiler v1.26.2" + }, + "$version": "1.0" +} diff --git a/src/test/resources/io/neonbee/endpoint/odatav4/io.neonbee.handler.empty.EmptyContainerService.edmx b/src/test/resources/io/neonbee/endpoint/odatav4/io.neonbee.handler.empty.EmptyContainerService.edmx new file mode 100644 index 000000000..92d7b2b6b --- /dev/null +++ b/src/test/resources/io/neonbee/endpoint/odatav4/io.neonbee.handler.empty.EmptyContainerService.edmx @@ -0,0 +1,6 @@ + + + + + +