From 22c06259d20058b4f4337e5f62512152d4de8211 Mon Sep 17 00:00:00 2001 From: C5371248 Date: Fri, 26 Jun 2026 15:54:54 +0300 Subject: [PATCH 1/3] fix: skip OData services with null EntityContainer to prevent NPE Services whose EDMX metadata has no EntityContainer are now filtered out during endpoint registration instead of causing a NullPointerException. Adds a warning log when an associated model path is missing and a null filter before collecting edmx models. Test coverage added via EmptyContainerService. --- .../endpoint/odatav4/ODataV4Endpoint.java | 8 ++++++++ .../io/neonbee/entity/EntityModelLoader.java | 8 +++++++- .../endpoint/odatav4/ODataV4EndpointTest.java | 20 ++++++++++++++++++- .../odatav4/EmptyContainerService.csn | 12 +++++++++++ ...e.handler.empty.EmptyContainerService.edmx | 6 ++++++ 5 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/test/resources/io/neonbee/endpoint/odatav4/EmptyContainerService.csn create mode 100644 src/test/resources/io/neonbee/endpoint/odatav4/io.neonbee.handler.empty.EmptyContainerService.edmx diff --git a/src/main/java/io/neonbee/endpoint/odatav4/ODataV4Endpoint.java b/src/main/java/io/neonbee/endpoint/odatav4/ODataV4Endpoint.java index 2b62f622d..067f97b15 100644 --- a/src/main/java/io/neonbee/endpoint/odatav4/ODataV4Endpoint.java +++ b/src/main/java/io/neonbee/endpoint/odatav4/ODataV4Endpoint.java @@ -268,6 +268,14 @@ 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) { + 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..6378b64d0 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.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(" + + + + + From 7557eb1e8fa3f7248104a646c068e8dec1037865 Mon Sep 17 00:00:00 2001 From: C5371248 Date: Fri, 26 Jun 2026 16:02:46 +0300 Subject: [PATCH 2/3] fix: wrap logger calls in level guards to satisfy PMD GuardLogStatement --- .../java/io/neonbee/endpoint/odatav4/ODataV4Endpoint.java | 6 ++++-- src/main/java/io/neonbee/entity/EntityModelLoader.java | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/neonbee/endpoint/odatav4/ODataV4Endpoint.java b/src/main/java/io/neonbee/endpoint/odatav4/ODataV4Endpoint.java index 067f97b15..a244e74f9 100644 --- a/src/main/java/io/neonbee/endpoint/odatav4/ODataV4Endpoint.java +++ b/src/main/java/io/neonbee/endpoint/odatav4/ODataV4Endpoint.java @@ -270,8 +270,10 @@ protected Future refreshRouter(Vertx vertx, Router router, String basePath .flatMap(entityModel -> entityModel.getAllEdmxMetadata().entrySet().stream()) .filter(entry -> { if (entry.getValue().getEdm().getEntityContainer() == null) { - LOGGER.error("Skipping OData service endpoint for {} — EntityContainer is null", - entry.getKey()); + if (LOGGER.isErrorEnabled()) { + LOGGER.error("Skipping OData service endpoint for {} — EntityContainer is null", + entry.getKey()); + } return false; } return true; diff --git a/src/main/java/io/neonbee/entity/EntityModelLoader.java b/src/main/java/io/neonbee/entity/EntityModelLoader.java index 6378b64d0..b1a338df4 100644 --- a/src/main/java/io/neonbee/entity/EntityModelLoader.java +++ b/src/main/java/io/neonbee/entity/EntityModelLoader.java @@ -217,7 +217,7 @@ Future parseModel(String csnFile, byte[] csnPayload, Map a .map(Path::toString).map(path -> { // we do not know if the path uses windows / unix path separators, try both! byte[] payload = FileSystemHelper.getPathFromMap(associatedModels, path); - if (payload == null) { + if (payload == null && LOGGER.isWarnEnabled()) { LOGGER.warn("Associated model {} not found in associatedModels map (keys={})", path, associatedModels.keySet()); } From 76523b955ff295508afd98fb643625f00e24e6e6 Mon Sep 17 00:00:00 2001 From: C5371248 Date: Fri, 26 Jun 2026 16:11:21 +0300 Subject: [PATCH 3/3] fix: update getCSNModelTest to assert on model content instead of object identity --- .../neonbee/entity/EntityModelLoaderTest.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/test/java/io/neonbee/entity/EntityModelLoaderTest.java b/src/test/java/io/neonbee/entity/EntityModelLoaderTest.java index c0dd1fdfb..f67faf9fe 100644 --- a/src/test/java/io/neonbee/entity/EntityModelLoaderTest.java +++ b/src/test/java/io/neonbee/entity/EntityModelLoaderTest.java @@ -16,7 +16,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; -import com.sap.cds.reflect.CdsModel; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsService; import io.neonbee.NeonBeeOptions; import io.neonbee.test.base.NeonBeeTestBase; @@ -120,12 +121,14 @@ void loadFromModuleTest(Vertx vertx, VertxTestContext testContext) throws IOExce @DisplayName("check if getting CSN Model works") void getCSNModelTest(Vertx vertx, VertxTestContext testContext) { EntityModelLoader loader = new EntityModelLoader(vertx); - Future 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(); }))); }