diff --git a/pom.xml b/pom.xml
index a39c8a0fccc31dd2d3e986a00ab1dc4069ff2adf..a4fa9f6aeb2846a0461d0fa6acf1da5d7e4965a9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -449,6 +449,14 @@
none
alphabetical
1
+ false
+
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApi.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApi.java
index 2de26104383c378d8a2b034566518b35e3eefd52..c6a77416a0f00744ac4532c75a3f201d70a2b0ad 100644
--- a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApi.java
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApi.java
@@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
+import java.util.List;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@@ -116,4 +117,30 @@ public interface HubApi {
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
+ @Operation(summary = "Get all registered listeners", operationId = "getListeners", description = "Retrieves all registered event subscriptions", tags={ "events subscription", })
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Success" ),
+ @ApiResponse(responseCode = "400", description = "Bad Request" ),
+ @ApiResponse(responseCode = "401", description = "Unauthorized" ),
+ @ApiResponse(responseCode = "403", description = "Forbidden" ),
+ @ApiResponse(responseCode = "500", description = "Internal Server Error" ) })
+ @RequestMapping(value = "/hub",
+ produces = { "application/json;charset=utf-8" },
+ method = RequestMethod.GET)
+ default ResponseEntity> getListeners() {
+ if(getObjectMapper().isPresent() && getAcceptHeader().isPresent()) {
+ if (getAcceptHeader().get().contains("application/json")) {
+ try {
+ return new ResponseEntity<>(getObjectMapper().get().readValue("[ { \"query\" : \"query\", \"callback\" : \"callback\", \"id\" : \"id\"} ]", List.class), HttpStatus.NOT_IMPLEMENTED);
+ } catch (IOException e) {
+ log.error("Couldn't serialize response for content type application/json", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+ } else {
+ log.warn("ObjectMapper or HttpServletRequest not configured in default HubApi interface so no example is generated");
+ }
+ return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
+ }
+
}
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java
index b15db91667abb9079a912edb82a5c803a9b1fed2..649fa9570c5bf9b723ec214ceef13bd61eb3fb2a 100644
--- a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java
@@ -19,6 +19,7 @@
*/
package org.etsi.osl.tmf.pcm620.api;
+import java.util.List;
import java.util.Optional;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -35,6 +36,7 @@ import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.servlet.http.HttpServletRequest;
@@ -70,8 +72,11 @@ public class HubApiController implements HubApi {
return Optional.ofNullable(request);
}
+ /*
+ * to register another OSL for example use "callback": "http://localhost:13082/tmf-api/productCatalogManagement/v4/"
+ */
@Override
- @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
+ @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')")
public ResponseEntity registerListener(@Parameter(description = "Data containing the callback endpoint to deliver the information", required = true) @Valid @RequestBody EventSubscriptionInput data) {
try {
EventSubscription eventSubscription = eventSubscriptionRepoService.addEventSubscription(data);
@@ -86,7 +91,8 @@ public class HubApiController implements HubApi {
}
@Override
- @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
+ @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')")
+ @RequestMapping(value = "/hub/{id}", method = RequestMethod.DELETE, produces = { "application/json;charset=utf-8" })
public ResponseEntity unregisterListener(@Parameter(description = "The id of the registered listener", required = true) @PathVariable("id") String id) {
try {
EventSubscription existing = eventSubscriptionRepoService.findById(id);
@@ -102,4 +108,16 @@ public class HubApiController implements HubApi {
}
}
+ @Override
+ @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')")
+ public ResponseEntity> getListeners() {
+ try {
+ List eventSubscriptions = eventSubscriptionRepoService.findAll();
+ return new ResponseEntity<>(eventSubscriptions, HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error retrieving listeners", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
}
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ListenerApiController.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ListenerApiController.java
index 77f66dba6f60dbdbbf072faafc1d03b133649088..ddd561725a55dd051afc4c6d9858fb44d4631c04 100644
--- a/src/main/java/org/etsi/osl/tmf/pcm620/api/ListenerApiController.java
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ListenerApiController.java
@@ -22,17 +22,41 @@ package org.etsi.osl.tmf.pcm620.api;
import java.util.Optional;
import com.fasterxml.jackson.databind.ObjectMapper;
-
+import org.etsi.osl.tmf.pcm620.model.CatalogBatchEvent;
+import org.etsi.osl.tmf.pcm620.model.CatalogCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.CategoryCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.CategoryDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.EventSubscription;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
+import io.swagger.v3.oas.annotations.Parameter;
import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.Valid;
@jakarta.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2019-10-19T00:15:57.249+03:00")
@Controller("ListenerApiController620")
@RequestMapping("/productCatalogManagement/v4/")
public class ListenerApiController implements ListenerApi {
+ private static final Logger log = LoggerFactory.getLogger(ListenerApiController.class);
+
private final ObjectMapper objectMapper;
private final HttpServletRequest request;
@@ -53,4 +77,184 @@ public class ListenerApiController implements ListenerApi {
return Optional.ofNullable(request);
}
+ @Override
+ public ResponseEntity listenToCatalogBatchEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody CatalogBatchEvent data) {
+ try {
+ log.info("Received CatalogBatchEvent: {}", data.getEventId());
+ log.debug("CatalogBatchEvent details: {}", data);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error processing CatalogBatchEvent", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity listenToCatalogCreateEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody CatalogCreateEvent data) {
+ try {
+ log.info("Received CatalogCreateEvent: {}", data.getEventId());
+ log.debug("CatalogCreateEvent details: {}", data);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error processing CatalogCreateEvent", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity listenToCatalogDeleteEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody CatalogDeleteEvent data) {
+ try {
+ log.info("Received CatalogDeleteEvent: {}", data.getEventId());
+ log.debug("CatalogDeleteEvent details: {}", data);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error processing CatalogDeleteEvent", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity listenToCategoryCreateEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody CategoryCreateEvent data) {
+ try {
+ log.info("Received CategoryCreateEvent: {}", data.getEventId());
+ log.debug("CategoryCreateEvent details: {}", data);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error processing CategoryCreateEvent", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity listenToCategoryDeleteEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody CategoryDeleteEvent data) {
+ try {
+ log.info("Received CategoryDeleteEvent: {}", data.getEventId());
+ log.debug("CategoryDeleteEvent details: {}", data);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error processing CategoryDeleteEvent", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity listenToProductOfferingAttributeValueChangeEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingAttributeValueChangeEvent data) {
+ try {
+ log.info("Received ProductOfferingAttributeValueChangeEvent: {}", data.getEventId());
+ log.debug("ProductOfferingAttributeValueChangeEvent details: {}", data);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error processing ProductOfferingAttributeValueChangeEvent", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity listenToProductOfferingCreateEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingCreateEvent data) {
+ try {
+ log.info("Received ProductOfferingCreateEvent: {}", data.getEventId());
+ log.debug("ProductOfferingCreateEvent details: {}", data);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error processing ProductOfferingCreateEvent", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity listenToProductOfferingDeleteEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingDeleteEvent data) {
+ try {
+ log.info("Received ProductOfferingDeleteEvent: {}", data.getEventId());
+ log.debug("ProductOfferingDeleteEvent details: {}", data);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error processing ProductOfferingDeleteEvent", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity listenToProductOfferingPriceAttributeValueChangeEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingPriceAttributeValueChangeEvent data) {
+ try {
+ log.info("Received ProductOfferingPriceAttributeValueChangeEvent: {}", data.getEventId());
+ log.debug("ProductOfferingPriceAttributeValueChangeEvent details: {}", data);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error processing ProductOfferingPriceAttributeValueChangeEvent", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity listenToProductOfferingPriceCreateEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingPriceCreateEvent data) {
+ try {
+ log.info("Received ProductOfferingPriceCreateEvent: {}", data.getEventId());
+ log.debug("ProductOfferingPriceCreateEvent details: {}", data);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error processing ProductOfferingPriceCreateEvent", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity listenToProductOfferingPriceDeleteEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingPriceDeleteEvent data) {
+ try {
+ log.info("Received ProductOfferingPriceDeleteEvent: {}", data.getEventId());
+ log.debug("ProductOfferingPriceDeleteEvent details: {}", data);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error processing ProductOfferingPriceDeleteEvent", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity listenToProductOfferingPriceStateChangeEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingPriceStateChangeEvent data) {
+ try {
+ log.info("Received ProductOfferingPriceStateChangeEvent: {}", data.getEventId());
+ log.debug("ProductOfferingPriceStateChangeEvent details: {}", data);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error processing ProductOfferingPriceStateChangeEvent", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity listenToProductOfferingStateChangeEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingStateChangeEvent data) {
+ try {
+ log.info("Received ProductOfferingStateChangeEvent: {}", data.getEventId());
+ log.debug("ProductOfferingStateChangeEvent details: {}", data);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error processing ProductOfferingStateChangeEvent", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity listenToProductSpecificationCreateEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductSpecificationCreateEvent data) {
+ try {
+ log.info("Received ProductSpecificationCreateEvent: {}", data.getEventId());
+ log.debug("ProductSpecificationCreateEvent details: {}", data);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error processing ProductSpecificationCreateEvent", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ public ResponseEntity listenToProductSpecificationDeleteEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductSpecificationDeleteEvent data) {
+ try {
+ log.info("Received ProductSpecificationDeleteEvent: {}", data.getEventId());
+ log.debug("ProductSpecificationDeleteEvent details: {}", data);
+ return new ResponseEntity<>(HttpStatus.OK);
+ } catch (Exception e) {
+ log.error("Error processing ProductSpecificationDeleteEvent", e);
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
}
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java
new file mode 100644
index 0000000000000000000000000000000000000000..f6c7b6e7244d68e36796bc0b5f38f6264a14fedc
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java
@@ -0,0 +1,189 @@
+/*-
+ * ========================LICENSE_START=================================
+ * org.etsi.osl.tmf.api
+ * %%
+ * Copyright (C) 2019 - 2020 openslice.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+package org.etsi.osl.tmf.pcm620.api;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.etsi.osl.centrallog.client.CLevel;
+import org.etsi.osl.centrallog.client.CentralLogger;
+import org.etsi.osl.tmf.common.model.Notification;
+import org.etsi.osl.tmf.pcm620.model.CatalogCreateNotification;
+import org.etsi.osl.tmf.pcm620.model.CatalogDeleteNotification;
+import org.etsi.osl.tmf.pcm620.model.CategoryCreateNotification;
+import org.etsi.osl.tmf.pcm620.model.CategoryDeleteNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeNotification;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Configuration
+@Component
+public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder {
+
+ private static final transient Log logger = LogFactory.getLog(ProductCatalogApiRouteBuilderEvents.class.getName());
+
+ @Value("${EVENT_PRODUCT_CATALOG_CREATE}")
+ private String EVENT_CATALOG_CREATE = "";
+
+ @Value("${EVENT_PRODUCT_CATALOG_DELETE}")
+ private String EVENT_CATALOG_DELETE = "";
+
+ @Value("${EVENT_PRODUCT_CATEGORY_CREATE}")
+ private String EVENT_CATEGORY_CREATE = "";
+
+ @Value("${EVENT_PRODUCT_CATEGORY_DELETE}")
+ private String EVENT_CATEGORY_DELETE = "";
+
+ @Value("${EVENT_PRODUCT_SPECIFICATION_CREATE}")
+ private String EVENT_PRODUCT_SPECIFICATION_CREATE = "";
+
+ @Value("${EVENT_PRODUCT_SPECIFICATION_DELETE}")
+ private String EVENT_PRODUCT_SPECIFICATION_DELETE = "";
+
+ @Value("${EVENT_PRODUCT_OFFERING_CREATE}")
+ private String EVENT_PRODUCT_OFFERING_CREATE = "";
+
+ @Value("${EVENT_PRODUCT_OFFERING_DELETE}")
+ private String EVENT_PRODUCT_OFFERING_DELETE = "";
+
+ @Value("${EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE}")
+ private String EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE = "";
+
+ @Value("${EVENT_PRODUCT_OFFERING_STATE_CHANGE}")
+ private String EVENT_PRODUCT_OFFERING_STATE_CHANGE = "";
+
+ @Value("${EVENT_PRODUCT_OFFERING_PRICE_CREATE}")
+ private String EVENT_PRODUCT_OFFERING_PRICE_CREATE = "";
+
+ @Value("${EVENT_PRODUCT_OFFERING_PRICE_DELETE}")
+ private String EVENT_PRODUCT_OFFERING_PRICE_DELETE = "";
+
+ @Value("${EVENT_PRODUCT_OFFERING_PRICE_ATTRIBUTE_VALUE_CHANGE}")
+ private String EVENT_PRODUCT_OFFERING_PRICE_ATTRIBUTE_VALUE_CHANGE = "";
+
+ @Value("${EVENT_PRODUCT_OFFERING_PRICE_STATE_CHANGE}")
+ private String EVENT_PRODUCT_OFFERING_PRICE_STATE_CHANGE = "";
+
+ @Value("${spring.application.name}")
+ private String compname;
+
+ @Autowired
+ private ProducerTemplate template;
+
+ @Autowired
+ private CentralLogger centralLogger;
+
+ @Override
+ public void configure() throws Exception {
+ // Configure routes for catalog events
+ }
+
+ /**
+ * Publish notification events for catalog operations
+ * @param n The notification to publish
+ * @param objId The catalog object ID
+ */
+ @Transactional
+ public void publishEvent(final Notification n, final String objId) {
+ n.setEventType(n.getClass().getName());
+ logger.info("will send Event for type " + n.getEventType());
+ try {
+ String msgtopic = "";
+
+ if (n instanceof CatalogCreateNotification) {
+ msgtopic = EVENT_CATALOG_CREATE;
+ } else if (n instanceof CatalogDeleteNotification) {
+ msgtopic = EVENT_CATALOG_DELETE;
+ } else if (n instanceof CategoryCreateNotification) {
+ msgtopic = EVENT_CATEGORY_CREATE;
+ } else if (n instanceof CategoryDeleteNotification) {
+ msgtopic = EVENT_CATEGORY_DELETE;
+ } else if (n instanceof ProductSpecificationCreateNotification) {
+ msgtopic = EVENT_PRODUCT_SPECIFICATION_CREATE;
+ } else if (n instanceof ProductSpecificationDeleteNotification) {
+ msgtopic = EVENT_PRODUCT_SPECIFICATION_DELETE;
+ } else if (n instanceof ProductOfferingCreateNotification) {
+ msgtopic = EVENT_PRODUCT_OFFERING_CREATE;
+ } else if (n instanceof ProductOfferingDeleteNotification) {
+ msgtopic = EVENT_PRODUCT_OFFERING_DELETE;
+ } else if (n instanceof ProductOfferingAttributeValueChangeNotification) {
+ msgtopic = EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE;
+ } else if (n instanceof ProductOfferingStateChangeNotification) {
+ msgtopic = EVENT_PRODUCT_OFFERING_STATE_CHANGE;
+ } else if (n instanceof ProductOfferingPriceCreateNotification) {
+ msgtopic = EVENT_PRODUCT_OFFERING_PRICE_CREATE;
+ } else if (n instanceof ProductOfferingPriceDeleteNotification) {
+ msgtopic = EVENT_PRODUCT_OFFERING_PRICE_DELETE;
+ } else if (n instanceof ProductOfferingPriceAttributeValueChangeNotification) {
+ msgtopic = EVENT_PRODUCT_OFFERING_PRICE_ATTRIBUTE_VALUE_CHANGE;
+ } else if (n instanceof ProductOfferingPriceStateChangeNotification) {
+ msgtopic = EVENT_PRODUCT_OFFERING_PRICE_STATE_CHANGE;
+ }
+
+ Map map = new HashMap<>();
+ map.put("eventid", n.getEventId());
+ map.put("objId", objId);
+
+ String apayload = toJsonString(n);
+ template.sendBodyAndHeaders(msgtopic, apayload, map);
+
+ centralLogger.log(CLevel.INFO, apayload, compname);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ logger.error("Cannot send Event . " + e.getMessage());
+ }
+ }
+
+ static String toJsonString(Object object) throws IOException {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.registerModule(new JavaTimeModule());
+ mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ return mapper.writeValueAsString(object);
+ }
+
+ static T toJsonObj(String content, Class valueType) throws IOException {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.registerModule(new JavaTimeModule());
+ mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ return mapper.readValue(content, valueType);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/configuration/RestTemplateConfig.java b/src/main/java/org/etsi/osl/tmf/pcm620/configuration/RestTemplateConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..d6240fcf50b2d2fbe0d9109704961b35fd23599e
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/configuration/RestTemplateConfig.java
@@ -0,0 +1,39 @@
+/*-
+ * ========================LICENSE_START=================================
+ * org.etsi.osl.tmf.api
+ * %%
+ * Copyright (C) 2019 - 2021 openslice.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+package org.etsi.osl.tmf.pcm620.configuration;
+
+import java.time.Duration;
+
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestTemplateConfig {
+
+ @Bean
+ public RestTemplate restTemplate(RestTemplateBuilder builder) {
+ return builder
+ .setConnectTimeout(Duration.ofSeconds(10))
+ .setReadTimeout(Duration.ofSeconds(30))
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java
new file mode 100644
index 0000000000000000000000000000000000000000..27e0e6cc1dd7c175014968f0227aa6acaf801f3a
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java
@@ -0,0 +1,159 @@
+/*-
+ * ========================LICENSE_START=================================
+ * org.etsi.osl.tmf.api
+ * %%
+ * Copyright (C) 2019 - 2021 openslice.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+package org.etsi.osl.tmf.pcm620.reposervices;
+
+import java.util.List;
+
+import org.etsi.osl.tmf.pcm620.model.CatalogCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.EventSubscription;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+@Service
+public class CatalogCallbackService {
+
+ private static final Logger logger = LoggerFactory.getLogger(CatalogCallbackService.class);
+
+ @Autowired
+ private EventSubscriptionRepoService eventSubscriptionRepoService;
+
+ @Autowired
+ private RestTemplate restTemplate;
+
+ /**
+ * Send catalog create event to all registered callback URLs
+ * @param catalogCreateEvent The catalog create event to send
+ */
+ public void sendCatalogCreateCallback(CatalogCreateEvent catalogCreateEvent) {
+ List subscriptions = eventSubscriptionRepoService.findAll();
+
+ for (EventSubscription subscription : subscriptions) {
+ if (shouldNotifySubscription(subscription, "catalogCreateEvent")) {
+ sendCatalogCreateEventToCallback(subscription.getCallback(), catalogCreateEvent);
+ }
+ }
+ }
+
+ /**
+ * Send catalog delete event to all registered callback URLs
+ * @param catalogDeleteEvent The catalog delete event to send
+ */
+ public void sendCatalogDeleteCallback(CatalogDeleteEvent catalogDeleteEvent) {
+ List subscriptions = eventSubscriptionRepoService.findAll();
+
+ for (EventSubscription subscription : subscriptions) {
+ if (shouldNotifySubscription(subscription, "catalogDeleteEvent")) {
+ sendCatalogDeleteEventToCallback(subscription.getCallback(), catalogDeleteEvent);
+ }
+ }
+ }
+
+ /**
+ * Send catalog create event to a specific callback URL
+ * @param callbackUrl The callback URL to send to
+ * @param event The catalog create event
+ */
+ private void sendCatalogCreateEventToCallback(String callbackUrl, CatalogCreateEvent event) {
+
+ String url = buildCallbackUrl(callbackUrl, "/listener/catalogCreateEvent");
+ try {
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity entity = new HttpEntity<>(event, headers);
+
+ ResponseEntity response = restTemplate.exchange(
+ url, HttpMethod.POST, entity, String.class);
+
+ logger.info("Successfully sent catalog create event to callback URL: {} - Response: {}",
+ url, response.getStatusCode());
+
+ } catch (Exception e) {
+ logger.error("Failed to send catalog create event to callback URL: {}", url, e);
+ }
+ }
+
+ /**
+ * Send catalog delete event to a specific callback URL
+ * @param callbackUrl The callback URL to send to
+ * @param event The catalog delete event
+ */
+ private void sendCatalogDeleteEventToCallback(String callbackUrl, CatalogDeleteEvent event) {
+ String url = buildCallbackUrl(callbackUrl, "/listener/catalogDeleteEvent");
+ try {
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity entity = new HttpEntity<>(event, headers);
+
+ ResponseEntity response = restTemplate.exchange(
+ url, HttpMethod.POST, entity, String.class);
+
+ logger.info("Successfully sent catalog delete event to callback URL: {} - Response: {}",
+ url, response.getStatusCode());
+
+ } catch (Exception e) {
+ logger.error("Failed to send catalog delete event to callback URL: {}", url, e);
+ }
+ }
+
+ /**
+ * Build the full callback URL with the listener endpoint
+ * @param baseUrl The base callback URL
+ * @param listenerPath The listener path to append
+ * @return The complete callback URL
+ */
+ private String buildCallbackUrl(String baseUrl, String listenerPath) {
+ if (baseUrl.endsWith("/")) {
+ return baseUrl.substring(0, baseUrl.length() - 1) + listenerPath;
+ } else {
+ return baseUrl + listenerPath;
+ }
+ }
+
+ /**
+ * Check if a subscription should be notified for a specific event type
+ * @param subscription The event subscription
+ * @param eventType The event type to check
+ * @return true if the subscription should be notified
+ */
+ private boolean shouldNotifySubscription(EventSubscription subscription, String eventType) {
+ // If no query is specified, notify all events
+ if (subscription.getQuery() == null || subscription.getQuery().trim().isEmpty()) {
+ return true;
+ }
+
+ // Check if the query contains the event type
+ String query = subscription.getQuery().toLowerCase();
+ return query.contains("catalog") ||
+ query.contains(eventType.toLowerCase()) ||
+ query.contains("catalog.create") ||
+ query.contains("catalog.delete");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java
new file mode 100644
index 0000000000000000000000000000000000000000..057f2039f07c4a8583596a7b55c27f616f7a7ddf
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java
@@ -0,0 +1,153 @@
+/*-
+ * ========================LICENSE_START=================================
+ * org.etsi.osl.tmf.api
+ * %%
+ * Copyright (C) 2019 - 2021 openslice.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+package org.etsi.osl.tmf.pcm620.reposervices;
+
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.UUID;
+
+import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents;
+import org.etsi.osl.tmf.pcm620.model.Catalog;
+import org.etsi.osl.tmf.pcm620.model.CatalogCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.CatalogCreateEventPayload;
+import org.etsi.osl.tmf.pcm620.model.CatalogCreateNotification;
+import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEventPayload;
+import org.etsi.osl.tmf.pcm620.model.CatalogDeleteNotification;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional
+public class CatalogNotificationService {
+
+ private static final Logger logger = LoggerFactory.getLogger(CatalogNotificationService.class);
+
+ @Autowired
+ private ProductCatalogApiRouteBuilderEvents eventPublisher;
+
+ @Autowired
+ private CatalogCallbackService catalogCallbackService;
+
+ /**
+ * Publish a catalog create notification
+ * @param catalog The created catalog
+ */
+ public void publishCatalogCreateNotification(Catalog catalog) {
+ try {
+ CatalogCreateNotification notification = createCatalogCreateNotification(catalog);
+ eventPublisher.publishEvent(notification, catalog.getUuid());
+
+ // Send callbacks to registered subscribers
+ if ( catalogCallbackService!=null )
+ catalogCallbackService.sendCatalogCreateCallback(notification.getEvent());
+
+ logger.info("Published catalog create notification for catalog ID: {}", catalog.getUuid());
+ } catch (Exception e) {
+ logger.error("Error publishing catalog create notification for catalog ID: {}", catalog.getUuid(), e);
+ }
+ }
+
+ /**
+ * Publish a catalog delete notification
+ * @param catalog The deleted catalog
+ */
+ public void publishCatalogDeleteNotification(Catalog catalog) {
+ try {
+ CatalogDeleteNotification notification = createCatalogDeleteNotification(catalog);
+ eventPublisher.publishEvent(notification, catalog.getUuid());
+
+ // Send callbacks to registered subscribers
+ if ( catalogCallbackService!=null )
+ catalogCallbackService.sendCatalogDeleteCallback(notification.getEvent());
+
+ logger.info("Published catalog delete notification for catalog ID: {}", catalog.getUuid());
+ } catch (Exception e) {
+ logger.error("Error publishing catalog delete notification for catalog ID: {}", catalog.getUuid(), e);
+ }
+ }
+
+ /**
+ * Create a catalog create notification
+ * @param catalog The created catalog
+ * @return CatalogCreateNotification
+ */
+ private CatalogCreateNotification createCatalogCreateNotification(Catalog catalog) {
+ CatalogCreateNotification notification = new CatalogCreateNotification();
+
+ // Set common notification properties
+ notification.setEventId(UUID.randomUUID().toString());
+ notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC));
+ notification.setEventType(CatalogCreateNotification.class.getName());
+ notification.setResourcePath("/productCatalogManagement/v4/catalog/" + catalog.getUuid());
+
+ // Create event
+ CatalogCreateEvent event = new CatalogCreateEvent();
+ event.setEventId(notification.getEventId());
+ event.setEventTime(notification.getEventTime());
+ event.setEventType("CatalogCreateEvent");
+ event.setTitle("Catalog Create Event");
+ event.setDescription("A catalog has been created");
+ event.setTimeOcurred(notification.getEventTime());
+
+ // Create event payload
+ CatalogCreateEventPayload payload = new CatalogCreateEventPayload();
+ payload.setCatalog(catalog);
+ event.setEvent(payload);
+
+ notification.setEvent(event);
+ return notification;
+ }
+
+ /**
+ * Create a catalog delete notification
+ * @param catalog The deleted catalog
+ * @return CatalogDeleteNotification
+ */
+ private CatalogDeleteNotification createCatalogDeleteNotification(Catalog catalog) {
+ CatalogDeleteNotification notification = new CatalogDeleteNotification();
+
+ // Set common notification properties
+ notification.setEventId(UUID.randomUUID().toString());
+ notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC));
+ notification.setEventType(CatalogDeleteNotification.class.getName());
+ notification.setResourcePath("/productCatalogManagement/v4/catalog/" + catalog.getUuid());
+
+ // Create event
+ CatalogDeleteEvent event = new CatalogDeleteEvent();
+ event.setEventId(notification.getEventId());
+ event.setEventTime(notification.getEventTime());
+ event.setEventType("CatalogDeleteEvent");
+ event.setTitle("Catalog Delete Event");
+ event.setDescription("A catalog has been deleted");
+ event.setTimeOcurred(notification.getEventTime());
+
+ // Create event payload
+ CatalogDeleteEventPayload payload = new CatalogDeleteEventPayload();
+ payload.setCatalog(catalog);
+ event.setEvent(payload);
+
+ notification.setEvent(event);
+ return notification;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryCallbackService.java
new file mode 100644
index 0000000000000000000000000000000000000000..4eec9f604215d2a06b141f573323e8ffa1961b86
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryCallbackService.java
@@ -0,0 +1,158 @@
+/*-
+ * ========================LICENSE_START=================================
+ * org.etsi.osl.tmf.api
+ * %%
+ * Copyright (C) 2019 - 2021 openslice.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+package org.etsi.osl.tmf.pcm620.reposervices;
+
+import java.util.List;
+
+import org.etsi.osl.tmf.pcm620.model.CategoryCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.CategoryDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.EventSubscription;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+@Service
+public class CategoryCallbackService {
+
+ private static final Logger logger = LoggerFactory.getLogger(CategoryCallbackService.class);
+
+ @Autowired
+ private EventSubscriptionRepoService eventSubscriptionRepoService;
+
+ @Autowired
+ private RestTemplate restTemplate;
+
+ /**
+ * Send category create event to all registered callback URLs
+ * @param categoryCreateEvent The category create event to send
+ */
+ public void sendCategoryCreateCallback(CategoryCreateEvent categoryCreateEvent) {
+ List subscriptions = eventSubscriptionRepoService.findAll();
+
+ for (EventSubscription subscription : subscriptions) {
+ if (shouldNotifySubscription(subscription, "categoryCreateEvent")) {
+ sendCategoryCreateEventToCallback(subscription.getCallback(), categoryCreateEvent);
+ }
+ }
+ }
+
+ /**
+ * Send category delete event to all registered callback URLs
+ * @param categoryDeleteEvent The category delete event to send
+ */
+ public void sendCategoryDeleteCallback(CategoryDeleteEvent categoryDeleteEvent) {
+ List subscriptions = eventSubscriptionRepoService.findAll();
+
+ for (EventSubscription subscription : subscriptions) {
+ if (shouldNotifySubscription(subscription, "categoryDeleteEvent")) {
+ sendCategoryDeleteEventToCallback(subscription.getCallback(), categoryDeleteEvent);
+ }
+ }
+ }
+
+ /**
+ * Send category create event to a specific callback URL
+ * @param callbackUrl The callback URL to send to
+ * @param event The category create event
+ */
+ private void sendCategoryCreateEventToCallback(String callbackUrl, CategoryCreateEvent event) {
+ try {
+ String url = buildCallbackUrl(callbackUrl, "/listener/categoryCreateEvent");
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity entity = new HttpEntity<>(event, headers);
+
+ ResponseEntity response = restTemplate.exchange(
+ url, HttpMethod.POST, entity, String.class);
+
+ logger.info("Successfully sent category create event to callback URL: {} - Response: {}",
+ url, response.getStatusCode());
+
+ } catch (Exception e) {
+ logger.error("Failed to send category create event to callback URL: {}", callbackUrl, e);
+ }
+ }
+
+ /**
+ * Send category delete event to a specific callback URL
+ * @param callbackUrl The callback URL to send to
+ * @param event The category delete event
+ */
+ private void sendCategoryDeleteEventToCallback(String callbackUrl, CategoryDeleteEvent event) {
+ try {
+ String url = buildCallbackUrl(callbackUrl, "/listener/categoryDeleteEvent");
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity entity = new HttpEntity<>(event, headers);
+
+ ResponseEntity response = restTemplate.exchange(
+ url, HttpMethod.POST, entity, String.class);
+
+ logger.info("Successfully sent category delete event to callback URL: {} - Response: {}",
+ url, response.getStatusCode());
+
+ } catch (Exception e) {
+ logger.error("Failed to send category delete event to callback URL: {}", callbackUrl, e);
+ }
+ }
+
+ /**
+ * Build the full callback URL with the listener endpoint
+ * @param baseUrl The base callback URL
+ * @param listenerPath The listener path to append
+ * @return The complete callback URL
+ */
+ private String buildCallbackUrl(String baseUrl, String listenerPath) {
+ if (baseUrl.endsWith("/")) {
+ return baseUrl.substring(0, baseUrl.length() - 1) + listenerPath;
+ } else {
+ return baseUrl + listenerPath;
+ }
+ }
+
+ /**
+ * Check if a subscription should be notified for a specific event type
+ * @param subscription The event subscription
+ * @param eventType The event type to check
+ * @return true if the subscription should be notified
+ */
+ private boolean shouldNotifySubscription(EventSubscription subscription, String eventType) {
+ // If no query is specified, notify all events
+ if (subscription.getQuery() == null || subscription.getQuery().trim().isEmpty()) {
+ return true;
+ }
+
+ // Check if the query contains the event type
+ String query = subscription.getQuery().toLowerCase();
+ return query.contains("category") ||
+ query.contains(eventType.toLowerCase()) ||
+ query.contains("category.create") ||
+ query.contains("category.delete");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryNotificationService.java
new file mode 100644
index 0000000000000000000000000000000000000000..4d200a58b0f1cc7e3ff60f227e51b48eefb6dd6d
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryNotificationService.java
@@ -0,0 +1,151 @@
+/*-
+ * ========================LICENSE_START=================================
+ * org.etsi.osl.tmf.api
+ * %%
+ * Copyright (C) 2019 - 2021 openslice.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+package org.etsi.osl.tmf.pcm620.reposervices;
+
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.UUID;
+
+import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents;
+import org.etsi.osl.tmf.pcm620.model.Category;
+import org.etsi.osl.tmf.pcm620.model.CategoryCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.CategoryCreateEventPayload;
+import org.etsi.osl.tmf.pcm620.model.CategoryCreateNotification;
+import org.etsi.osl.tmf.pcm620.model.CategoryDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.CategoryDeleteEventPayload;
+import org.etsi.osl.tmf.pcm620.model.CategoryDeleteNotification;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional
+public class CategoryNotificationService {
+
+ private static final Logger logger = LoggerFactory.getLogger(CategoryNotificationService.class);
+
+ @Autowired
+ private ProductCatalogApiRouteBuilderEvents eventPublisher;
+
+ @Autowired
+ private CategoryCallbackService categoryCallbackService;
+
+ /**
+ * Publish a category create notification
+ * @param category The created category
+ */
+ public void publishCategoryCreateNotification(Category category) {
+ try {
+ CategoryCreateNotification notification = createCategoryCreateNotification(category);
+ eventPublisher.publishEvent(notification, category.getUuid());
+
+ // Send callbacks to registered subscribers
+ categoryCallbackService.sendCategoryCreateCallback(notification.getEvent());
+
+ logger.info("Published category create notification for category ID: {}", category.getUuid());
+ } catch (Exception e) {
+ logger.error("Error publishing category create notification for category ID: {}", category.getUuid(), e);
+ }
+ }
+
+ /**
+ * Publish a category delete notification
+ * @param category The deleted category
+ */
+ public void publishCategoryDeleteNotification(Category category) {
+ try {
+ CategoryDeleteNotification notification = createCategoryDeleteNotification(category);
+ eventPublisher.publishEvent(notification, category.getUuid());
+
+ // Send callbacks to registered subscribers
+ categoryCallbackService.sendCategoryDeleteCallback(notification.getEvent());
+
+ logger.info("Published category delete notification for category ID: {}", category.getUuid());
+ } catch (Exception e) {
+ logger.error("Error publishing category delete notification for category ID: {}", category.getUuid(), e);
+ }
+ }
+
+ /**
+ * Create a category create notification
+ * @param category The created category
+ * @return CategoryCreateNotification
+ */
+ private CategoryCreateNotification createCategoryCreateNotification(Category category) {
+ CategoryCreateNotification notification = new CategoryCreateNotification();
+
+ // Set common notification properties
+ notification.setEventId(UUID.randomUUID().toString());
+ notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC));
+ notification.setEventType(CategoryCreateNotification.class.getName());
+ notification.setResourcePath("/productCatalogManagement/v4/category/" + category.getUuid());
+
+ // Create event
+ CategoryCreateEvent event = new CategoryCreateEvent();
+ event.setEventId(notification.getEventId());
+ event.setEventTime(notification.getEventTime());
+ event.setEventType("CategoryCreateEvent");
+ event.setTitle("Category Create Event");
+ event.setDescription("A category has been created");
+ event.setTimeOcurred(notification.getEventTime());
+
+ // Create event payload
+ CategoryCreateEventPayload payload = new CategoryCreateEventPayload();
+ payload.setCategory(category);
+ event.setEvent(payload);
+
+ notification.setEvent(event);
+ return notification;
+ }
+
+ /**
+ * Create a category delete notification
+ * @param category The deleted category
+ * @return CategoryDeleteNotification
+ */
+ private CategoryDeleteNotification createCategoryDeleteNotification(Category category) {
+ CategoryDeleteNotification notification = new CategoryDeleteNotification();
+
+ // Set common notification properties
+ notification.setEventId(UUID.randomUUID().toString());
+ notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC));
+ notification.setEventType(CategoryDeleteNotification.class.getName());
+ notification.setResourcePath("/productCatalogManagement/v4/category/" + category.getUuid());
+
+ // Create event
+ CategoryDeleteEvent event = new CategoryDeleteEvent();
+ event.setEventId(notification.getEventId());
+ event.setEventTime(notification.getEventTime());
+ event.setEventType("CategoryDeleteEvent");
+ event.setTitle("Category Delete Event");
+ event.setDescription("A category has been deleted");
+ event.setTimeOcurred(notification.getEventTime());
+
+ // Create event payload
+ CategoryDeleteEventPayload payload = new CategoryDeleteEventPayload();
+ payload.setCategory(category);
+ event.setEvent(payload);
+
+ notification.setEvent(event);
+ return notification;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java
index 8fc9caad8102825fb4f48d8f46570d0bc4eb2b11..4f5c85952da2a48696ce77e8f59d7aa3bc5bb644 100644
--- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java
@@ -19,6 +19,7 @@
*/
package org.etsi.osl.tmf.pcm620.reposervices;
+import java.util.List;
import java.util.Optional;
import java.util.UUID;
@@ -62,4 +63,8 @@ public class EventSubscriptionRepoService {
this.eventSubscriptionRepo.delete(optionalEventSubscription.get());
}
}
+
+ public List findAll() {
+ return (List) this.eventSubscriptionRepo.findAll();
+ }
}
\ No newline at end of file
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCatalogRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCatalogRepoService.java
index 3f66f5c7686023d4734ae40376100f99e179e269..71bfe6fe3418d619b594ebea128edc4463a1945a 100644
--- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCatalogRepoService.java
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCatalogRepoService.java
@@ -53,11 +53,18 @@ public class ProductCatalogRepoService {
@Autowired
ProductCategoryRepoService categRepoService;
+
+ @Autowired
+ CatalogNotificationService catalogNotificationService;
public Catalog addCatalog(Catalog c) {
-
- return this.catalogRepo.save(c);
+ Catalog savedCatalog = this.catalogRepo.save(c);
+
+ // Publish catalog create notification
+ catalogNotificationService.publishCatalogCreateNotification(savedCatalog);
+
+ return savedCatalog;
}
public Catalog addCatalog(@Valid CatalogCreate serviceCat) {
@@ -65,7 +72,12 @@ public class ProductCatalogRepoService {
Catalog sc = new Catalog();
sc = updateCatalogDataFromAPICall(sc, serviceCat);
- return this.catalogRepo.save(sc);
+ Catalog savedCatalog = this.catalogRepo.save(sc);
+
+ // Publish catalog create notification
+ catalogNotificationService.publishCatalogCreateNotification(savedCatalog);
+
+ return savedCatalog;
}
public List findAll() {
@@ -85,9 +97,15 @@ public class ProductCatalogRepoService {
public Void deleteById(String id) {
Optional optionalCat = this.catalogRepo.findByUuid(id);
- this.catalogRepo.delete(optionalCat.get());
+ if (optionalCat.isPresent()) {
+ Catalog catalogToDelete = optionalCat.get();
+
+ // Publish catalog delete notification before deletion
+ catalogNotificationService.publishCatalogDeleteNotification(catalogToDelete);
+
+ this.catalogRepo.delete(catalogToDelete);
+ }
return null;
-
}
public String findByUuidEager(String id) {
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java
index c7e226c03bbea2d617b95b6c1c4dae13b8820053..ca2355d3b88ffa51c5acef1de5d37ec2633ac215 100644
--- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java
@@ -61,6 +61,9 @@ public class ProductCategoryRepoService {
private final ProductCategoriesRepository categsRepo;
private final ProductOfferingRepository prodsOfferingRepo;
+
+ @Autowired
+ private CategoryNotificationService categoryNotificationService;
/**
* from
@@ -78,8 +81,14 @@ public class ProductCategoryRepoService {
public Category addCategory(Category c) {
-
- return this.categsRepo.save( c );
+ Category savedCategory = this.categsRepo.save( c );
+
+ // Publish category create notification
+ if (categoryNotificationService != null) {
+ categoryNotificationService.publishCategoryCreateNotification(savedCategory);
+ }
+
+ return savedCategory;
}
public Category addCategory(@Valid CategoryCreate Category) {
@@ -87,7 +96,14 @@ public class ProductCategoryRepoService {
Category sc = new Category() ;
sc = updateCategoryDataFromAPICall(sc, Category);
- return this.categsRepo.save( sc );
+ Category savedCategory = this.categsRepo.save( sc );
+
+ // Publish category create notification
+ if (categoryNotificationService != null) {
+ categoryNotificationService.publishCategoryCreateNotification(savedCategory);
+ }
+
+ return savedCategory;
}
@@ -138,13 +154,23 @@ public class ProductCategoryRepoService {
return false; //has children
}
+ Category categoryToDelete = optionalCat.get();
+
+ // Trigger lazy loading of associations before deletion to avoid lazy initialization exception
+ categoryToDelete.getProductOfferingObj().size(); // This will initialize the lazy collection
+ categoryToDelete.getCategoryObj().size(); // This will initialize the lazy collection
+
+ // Publish category delete notification BEFORE deletion to ensure session is still active
+ if (categoryNotificationService != null) {
+ categoryNotificationService.publishCategoryDeleteNotification(categoryToDelete);
+ }
- if ( optionalCat.get().getParentId() != null ) {
- Category parentCat = (this.categsRepo.findByUuid( optionalCat.get().getParentId() )).get();
+ if ( categoryToDelete.getParentId() != null ) {
+ Category parentCat = (this.categsRepo.findByUuid( categoryToDelete.getParentId() )).get();
//remove from parent category
for (Category ss : parentCat.getCategoryObj()) {
- if ( ss.getId() == optionalCat.get().getId() ) {
+ if ( ss.getId() == categoryToDelete.getId() ) {
parentCat.getCategoryObj().remove(ss);
break;
}
@@ -152,8 +178,8 @@ public class ProductCategoryRepoService {
parentCat = this.categsRepo.save(parentCat);
}
+ this.categsRepo.delete( categoryToDelete);
- this.categsRepo.delete( optionalCat.get());
return true;
}
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingCallbackService.java
new file mode 100644
index 0000000000000000000000000000000000000000..f6008a083c758181ef8a98385103d72152ab8694
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingCallbackService.java
@@ -0,0 +1,238 @@
+/*-
+ * ========================LICENSE_START=================================
+ * org.etsi.osl.tmf.api
+ * %%
+ * Copyright (C) 2019 - 2021 openslice.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+package org.etsi.osl.tmf.pcm620.reposervices;
+
+import java.util.List;
+
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.EventSubscription;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+@Service
+public class ProductOfferingCallbackService {
+
+ private static final Logger logger = LoggerFactory.getLogger(ProductOfferingCallbackService.class);
+
+ @Autowired
+ private EventSubscriptionRepoService eventSubscriptionRepoService;
+
+ @Autowired
+ private RestTemplate restTemplate;
+
+ /**
+ * Send product offering create event to all registered callback URLs
+ * @param productOfferingCreateEvent The product offering create event to send
+ */
+ public void sendProductOfferingCreateCallback(ProductOfferingCreateEvent productOfferingCreateEvent) {
+ List subscriptions = eventSubscriptionRepoService.findAll();
+
+ for (EventSubscription subscription : subscriptions) {
+ if (shouldNotifySubscription(subscription, "productOfferingCreateEvent")) {
+ sendProductOfferingCreateEventToCallback(subscription.getCallback(), productOfferingCreateEvent);
+ }
+ }
+ }
+
+ /**
+ * Send product offering delete event to all registered callback URLs
+ * @param productOfferingDeleteEvent The product offering delete event to send
+ */
+ public void sendProductOfferingDeleteCallback(ProductOfferingDeleteEvent productOfferingDeleteEvent) {
+ List subscriptions = eventSubscriptionRepoService.findAll();
+
+ for (EventSubscription subscription : subscriptions) {
+ if (shouldNotifySubscription(subscription, "productOfferingDeleteEvent")) {
+ sendProductOfferingDeleteEventToCallback(subscription.getCallback(), productOfferingDeleteEvent);
+ }
+ }
+ }
+
+ /**
+ * Send product offering attribute value change event to all registered callback URLs
+ * @param productOfferingAttributeValueChangeEvent The product offering attribute value change event to send
+ */
+ public void sendProductOfferingAttributeValueChangeCallback(ProductOfferingAttributeValueChangeEvent productOfferingAttributeValueChangeEvent) {
+ List subscriptions = eventSubscriptionRepoService.findAll();
+
+ for (EventSubscription subscription : subscriptions) {
+ if (shouldNotifySubscription(subscription, "productOfferingAttributeValueChangeEvent")) {
+ sendProductOfferingAttributeValueChangeEventToCallback(subscription.getCallback(), productOfferingAttributeValueChangeEvent);
+ }
+ }
+ }
+
+ /**
+ * Send product offering state change event to all registered callback URLs
+ * @param productOfferingStateChangeEvent The product offering state change event to send
+ */
+ public void sendProductOfferingStateChangeCallback(ProductOfferingStateChangeEvent productOfferingStateChangeEvent) {
+ List subscriptions = eventSubscriptionRepoService.findAll();
+
+ for (EventSubscription subscription : subscriptions) {
+ if (shouldNotifySubscription(subscription, "productOfferingStateChangeEvent")) {
+ sendProductOfferingStateChangeEventToCallback(subscription.getCallback(), productOfferingStateChangeEvent);
+ }
+ }
+ }
+
+ /**
+ * Send product offering create event to a specific callback URL
+ * @param callbackUrl The callback URL to send to
+ * @param event The product offering create event
+ */
+ private void sendProductOfferingCreateEventToCallback(String callbackUrl, ProductOfferingCreateEvent event) {
+ try {
+ String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingCreateEvent");
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity entity = new HttpEntity<>(event, headers);
+
+ ResponseEntity response = restTemplate.exchange(
+ url, HttpMethod.POST, entity, String.class);
+
+ logger.info("Successfully sent product offering create event to callback URL: {} - Response: {}",
+ url, response.getStatusCode());
+
+ } catch (Exception e) {
+ logger.error("Failed to send product offering create event to callback URL: {}", callbackUrl, e);
+ }
+ }
+
+ /**
+ * Send product offering delete event to a specific callback URL
+ * @param callbackUrl The callback URL to send to
+ * @param event The product offering delete event
+ */
+ private void sendProductOfferingDeleteEventToCallback(String callbackUrl, ProductOfferingDeleteEvent event) {
+ try {
+ String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingDeleteEvent");
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity entity = new HttpEntity<>(event, headers);
+
+ ResponseEntity response = restTemplate.exchange(
+ url, HttpMethod.POST, entity, String.class);
+
+ logger.info("Successfully sent product offering delete event to callback URL: {} - Response: {}",
+ url, response.getStatusCode());
+
+ } catch (Exception e) {
+ logger.error("Failed to send product offering delete event to callback URL: {}", callbackUrl, e);
+ }
+ }
+
+ /**
+ * Send product offering attribute value change event to a specific callback URL
+ * @param callbackUrl The callback URL to send to
+ * @param event The product offering attribute value change event
+ */
+ private void sendProductOfferingAttributeValueChangeEventToCallback(String callbackUrl, ProductOfferingAttributeValueChangeEvent event) {
+ try {
+ String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingAttributeValueChangeEvent");
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity entity = new HttpEntity<>(event, headers);
+
+ ResponseEntity response = restTemplate.exchange(
+ url, HttpMethod.POST, entity, String.class);
+
+ logger.info("Successfully sent product offering attribute value change event to callback URL: {} - Response: {}",
+ url, response.getStatusCode());
+
+ } catch (Exception e) {
+ logger.error("Failed to send product offering attribute value change event to callback URL: {}", callbackUrl, e);
+ }
+ }
+
+ /**
+ * Send product offering state change event to a specific callback URL
+ * @param callbackUrl The callback URL to send to
+ * @param event The product offering state change event
+ */
+ private void sendProductOfferingStateChangeEventToCallback(String callbackUrl, ProductOfferingStateChangeEvent event) {
+ try {
+ String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingStateChangeEvent");
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity entity = new HttpEntity<>(event, headers);
+
+ ResponseEntity response = restTemplate.exchange(
+ url, HttpMethod.POST, entity, String.class);
+
+ logger.info("Successfully sent product offering state change event to callback URL: {} - Response: {}",
+ url, response.getStatusCode());
+
+ } catch (Exception e) {
+ logger.error("Failed to send product offering state change event to callback URL: {}", callbackUrl, e);
+ }
+ }
+
+ /**
+ * Build the full callback URL with the listener endpoint
+ * @param baseUrl The base callback URL
+ * @param listenerPath The listener path to append
+ * @return The complete callback URL
+ */
+ private String buildCallbackUrl(String baseUrl, String listenerPath) {
+ if (baseUrl.endsWith("/")) {
+ return baseUrl.substring(0, baseUrl.length() - 1) + listenerPath;
+ } else {
+ return baseUrl + listenerPath;
+ }
+ }
+
+ /**
+ * Check if a subscription should be notified for a specific event type
+ * @param subscription The event subscription
+ * @param eventType The event type to check
+ * @return true if the subscription should be notified
+ */
+ private boolean shouldNotifySubscription(EventSubscription subscription, String eventType) {
+ // If no query is specified, notify all events
+ if (subscription.getQuery() == null || subscription.getQuery().trim().isEmpty()) {
+ return true;
+ }
+
+ // Check if the query contains the event type
+ String query = subscription.getQuery().toLowerCase();
+ return query.contains("productoffering") ||
+ query.contains(eventType.toLowerCase()) ||
+ query.contains("productoffering.create") ||
+ query.contains("productoffering.delete") ||
+ query.contains("productoffering.attributevaluechange") ||
+ query.contains("productoffering.statechange");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingNotificationService.java
new file mode 100644
index 0000000000000000000000000000000000000000..e10e25f0eab31a7144e79cfdb1f5fda9afbbb8ce
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingNotificationService.java
@@ -0,0 +1,257 @@
+/*-
+ * ========================LICENSE_START=================================
+ * org.etsi.osl.tmf.api
+ * %%
+ * Copyright (C) 2019 - 2021 openslice.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+package org.etsi.osl.tmf.pcm620.reposervices;
+
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.UUID;
+
+import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents;
+import org.etsi.osl.tmf.pcm620.model.ProductOffering;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateEventPayload;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteEventPayload;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeEventPayload;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeEventPayload;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeNotification;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional
+public class ProductOfferingNotificationService {
+
+ private static final Logger logger = LoggerFactory.getLogger(ProductOfferingNotificationService.class);
+
+ @Autowired
+ private ProductCatalogApiRouteBuilderEvents eventPublisher;
+
+ @Autowired
+ private ProductOfferingCallbackService productOfferingCallbackService;
+
+ /**
+ * Publish a product offering create notification
+ * @param productOffering The created product offering
+ */
+ public void publishProductOfferingCreateNotification(ProductOffering productOffering) {
+ try {
+ ProductOfferingCreateNotification notification = createProductOfferingCreateNotification(productOffering);
+ eventPublisher.publishEvent(notification, productOffering.getUuid());
+
+ // Send callbacks to registered subscribers
+ productOfferingCallbackService.sendProductOfferingCreateCallback(notification.getEvent());
+
+ logger.info("Published product offering create notification for product offering ID: {}", productOffering.getUuid());
+ } catch (Exception e) {
+ logger.error("Error publishing product offering create notification for product offering ID: {}", productOffering.getUuid(), e);
+ }
+ }
+
+ /**
+ * Publish a product offering delete notification
+ * @param productOffering The deleted product offering
+ */
+ public void publishProductOfferingDeleteNotification(ProductOffering productOffering) {
+ try {
+ ProductOfferingDeleteNotification notification = createProductOfferingDeleteNotification(productOffering);
+ eventPublisher.publishEvent(notification, productOffering.getUuid());
+
+ // Send callbacks to registered subscribers
+ productOfferingCallbackService.sendProductOfferingDeleteCallback(notification.getEvent());
+
+ logger.info("Published product offering delete notification for product offering ID: {}", productOffering.getUuid());
+ } catch (Exception e) {
+ logger.error("Error publishing product offering delete notification for product offering ID: {}", productOffering.getUuid(), e);
+ }
+ }
+
+ /**
+ * Publish a product offering attribute value change notification
+ * @param productOffering The product offering with changed attributes
+ */
+ public void publishProductOfferingAttributeValueChangeNotification(ProductOffering productOffering) {
+ try {
+ ProductOfferingAttributeValueChangeNotification notification = createProductOfferingAttributeValueChangeNotification(productOffering);
+ eventPublisher.publishEvent(notification, productOffering.getUuid());
+
+ // Send callbacks to registered subscribers
+ productOfferingCallbackService.sendProductOfferingAttributeValueChangeCallback(notification.getEvent());
+
+ logger.info("Published product offering attribute value change notification for product offering ID: {}", productOffering.getUuid());
+ } catch (Exception e) {
+ logger.error("Error publishing product offering attribute value change notification for product offering ID: {}", productOffering.getUuid(), e);
+ }
+ }
+
+ /**
+ * Publish a product offering state change notification
+ * @param productOffering The product offering with changed state
+ */
+ public void publishProductOfferingStateChangeNotification(ProductOffering productOffering) {
+ try {
+ ProductOfferingStateChangeNotification notification = createProductOfferingStateChangeNotification(productOffering);
+ eventPublisher.publishEvent(notification, productOffering.getUuid());
+
+ // Send callbacks to registered subscribers
+ productOfferingCallbackService.sendProductOfferingStateChangeCallback(notification.getEvent());
+
+ logger.info("Published product offering state change notification for product offering ID: {}", productOffering.getUuid());
+ } catch (Exception e) {
+ logger.error("Error publishing product offering state change notification for product offering ID: {}", productOffering.getUuid(), e);
+ }
+ }
+
+ /**
+ * Create a product offering create notification
+ * @param productOffering The created product offering
+ * @return ProductOfferingCreateNotification
+ */
+ private ProductOfferingCreateNotification createProductOfferingCreateNotification(ProductOffering productOffering) {
+ ProductOfferingCreateNotification notification = new ProductOfferingCreateNotification();
+
+ // Set common notification properties
+ notification.setEventId(UUID.randomUUID().toString());
+ notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC));
+ notification.setEventType(ProductOfferingCreateNotification.class.getName());
+ notification.setResourcePath("/productCatalogManagement/v4/productOffering/" + productOffering.getUuid());
+
+ // Create event
+ ProductOfferingCreateEvent event = new ProductOfferingCreateEvent();
+ event.setEventId(notification.getEventId());
+ event.setEventTime(notification.getEventTime());
+ event.setEventType("ProductOfferingCreateEvent");
+ event.setTitle("Product Offering Create Event");
+ event.setDescription("A product offering has been created");
+ event.setTimeOcurred(notification.getEventTime());
+
+ // Create event payload
+ ProductOfferingCreateEventPayload payload = new ProductOfferingCreateEventPayload();
+ payload.setProductOffering(productOffering);
+ event.setEvent(payload);
+
+ notification.setEvent(event);
+ return notification;
+ }
+
+ /**
+ * Create a product offering delete notification
+ * @param productOffering The deleted product offering
+ * @return ProductOfferingDeleteNotification
+ */
+ private ProductOfferingDeleteNotification createProductOfferingDeleteNotification(ProductOffering productOffering) {
+ ProductOfferingDeleteNotification notification = new ProductOfferingDeleteNotification();
+
+ // Set common notification properties
+ notification.setEventId(UUID.randomUUID().toString());
+ notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC));
+ notification.setEventType(ProductOfferingDeleteNotification.class.getName());
+ notification.setResourcePath("/productCatalogManagement/v4/productOffering/" + productOffering.getUuid());
+
+ // Create event
+ ProductOfferingDeleteEvent event = new ProductOfferingDeleteEvent();
+ event.setEventId(notification.getEventId());
+ event.setEventTime(notification.getEventTime());
+ event.setEventType("ProductOfferingDeleteEvent");
+ event.setTitle("Product Offering Delete Event");
+ event.setDescription("A product offering has been deleted");
+ event.setTimeOcurred(notification.getEventTime());
+
+ // Create event payload
+ ProductOfferingDeleteEventPayload payload = new ProductOfferingDeleteEventPayload();
+ payload.setProductOffering(productOffering);
+ event.setEvent(payload);
+
+ notification.setEvent(event);
+ return notification;
+ }
+
+ /**
+ * Create a product offering attribute value change notification
+ * @param productOffering The product offering with changed attributes
+ * @return ProductOfferingAttributeValueChangeNotification
+ */
+ private ProductOfferingAttributeValueChangeNotification createProductOfferingAttributeValueChangeNotification(ProductOffering productOffering) {
+ ProductOfferingAttributeValueChangeNotification notification = new ProductOfferingAttributeValueChangeNotification();
+
+ // Set common notification properties
+ notification.setEventId(UUID.randomUUID().toString());
+ notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC));
+ notification.setEventType(ProductOfferingAttributeValueChangeNotification.class.getName());
+ notification.setResourcePath("/productCatalogManagement/v4/productOffering/" + productOffering.getUuid());
+
+ // Create event
+ ProductOfferingAttributeValueChangeEvent event = new ProductOfferingAttributeValueChangeEvent();
+ event.setEventId(notification.getEventId());
+ event.setEventTime(notification.getEventTime());
+ event.setEventType("ProductOfferingAttributeValueChangeEvent");
+ event.setTitle("Product Offering Attribute Value Change Event");
+ event.setDescription("A product offering attribute value has been changed");
+ event.setTimeOcurred(notification.getEventTime());
+
+ // Create event payload
+ ProductOfferingAttributeValueChangeEventPayload payload = new ProductOfferingAttributeValueChangeEventPayload();
+ payload.setProductOffering(productOffering);
+ event.setEvent(payload);
+
+ notification.setEvent(event);
+ return notification;
+ }
+
+ /**
+ * Create a product offering state change notification
+ * @param productOffering The product offering with changed state
+ * @return ProductOfferingStateChangeNotification
+ */
+ private ProductOfferingStateChangeNotification createProductOfferingStateChangeNotification(ProductOffering productOffering) {
+ ProductOfferingStateChangeNotification notification = new ProductOfferingStateChangeNotification();
+
+ // Set common notification properties
+ notification.setEventId(UUID.randomUUID().toString());
+ notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC));
+ notification.setEventType(ProductOfferingStateChangeNotification.class.getName());
+ notification.setResourcePath("/productCatalogManagement/v4/productOffering/" + productOffering.getUuid());
+
+ // Create event
+ ProductOfferingStateChangeEvent event = new ProductOfferingStateChangeEvent();
+ event.setEventId(notification.getEventId());
+ event.setEventTime(notification.getEventTime());
+ event.setEventType("ProductOfferingStateChangeEvent");
+ event.setTitle("Product Offering State Change Event");
+ event.setDescription("A product offering state has been changed");
+ event.setTimeOcurred(notification.getEventTime());
+
+ // Create event payload
+ ProductOfferingStateChangeEventPayload payload = new ProductOfferingStateChangeEventPayload();
+ payload.setProductOffering(productOffering);
+ event.setEvent(payload);
+
+ notification.setEvent(event);
+ return notification;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceCallbackService.java
new file mode 100644
index 0000000000000000000000000000000000000000..f1533850170c174a0ff678341918732667e43c06
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceCallbackService.java
@@ -0,0 +1,238 @@
+/*-
+ * ========================LICENSE_START=================================
+ * org.etsi.osl.tmf.api
+ * %%
+ * Copyright (C) 2019 - 2021 openslice.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+package org.etsi.osl.tmf.pcm620.reposervices;
+
+import java.util.List;
+
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.EventSubscription;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+@Service
+public class ProductOfferingPriceCallbackService {
+
+ private static final Logger logger = LoggerFactory.getLogger(ProductOfferingPriceCallbackService.class);
+
+ @Autowired
+ private EventSubscriptionRepoService eventSubscriptionRepoService;
+
+ @Autowired
+ private RestTemplate restTemplate;
+
+ /**
+ * Send product offering price create event to all registered callback URLs
+ * @param productOfferingPriceCreateEvent The product offering price create event to send
+ */
+ public void sendProductOfferingPriceCreateCallback(ProductOfferingPriceCreateEvent productOfferingPriceCreateEvent) {
+ List subscriptions = eventSubscriptionRepoService.findAll();
+
+ for (EventSubscription subscription : subscriptions) {
+ if (shouldNotifySubscription(subscription, "productOfferingPriceCreateEvent")) {
+ sendProductOfferingPriceCreateEventToCallback(subscription.getCallback(), productOfferingPriceCreateEvent);
+ }
+ }
+ }
+
+ /**
+ * Send product offering price delete event to all registered callback URLs
+ * @param productOfferingPriceDeleteEvent The product offering price delete event to send
+ */
+ public void sendProductOfferingPriceDeleteCallback(ProductOfferingPriceDeleteEvent productOfferingPriceDeleteEvent) {
+ List subscriptions = eventSubscriptionRepoService.findAll();
+
+ for (EventSubscription subscription : subscriptions) {
+ if (shouldNotifySubscription(subscription, "productOfferingPriceDeleteEvent")) {
+ sendProductOfferingPriceDeleteEventToCallback(subscription.getCallback(), productOfferingPriceDeleteEvent);
+ }
+ }
+ }
+
+ /**
+ * Send product offering price attribute value change event to all registered callback URLs
+ * @param productOfferingPriceAttributeValueChangeEvent The product offering price attribute value change event to send
+ */
+ public void sendProductOfferingPriceAttributeValueChangeCallback(ProductOfferingPriceAttributeValueChangeEvent productOfferingPriceAttributeValueChangeEvent) {
+ List subscriptions = eventSubscriptionRepoService.findAll();
+
+ for (EventSubscription subscription : subscriptions) {
+ if (shouldNotifySubscription(subscription, "productOfferingPriceAttributeValueChangeEvent")) {
+ sendProductOfferingPriceAttributeValueChangeEventToCallback(subscription.getCallback(), productOfferingPriceAttributeValueChangeEvent);
+ }
+ }
+ }
+
+ /**
+ * Send product offering price state change event to all registered callback URLs
+ * @param productOfferingPriceStateChangeEvent The product offering price state change event to send
+ */
+ public void sendProductOfferingPriceStateChangeCallback(ProductOfferingPriceStateChangeEvent productOfferingPriceStateChangeEvent) {
+ List subscriptions = eventSubscriptionRepoService.findAll();
+
+ for (EventSubscription subscription : subscriptions) {
+ if (shouldNotifySubscription(subscription, "productOfferingPriceStateChangeEvent")) {
+ sendProductOfferingPriceStateChangeEventToCallback(subscription.getCallback(), productOfferingPriceStateChangeEvent);
+ }
+ }
+ }
+
+ /**
+ * Send product offering price create event to a specific callback URL
+ * @param callbackUrl The callback URL to send to
+ * @param event The product offering price create event
+ */
+ private void sendProductOfferingPriceCreateEventToCallback(String callbackUrl, ProductOfferingPriceCreateEvent event) {
+ try {
+ String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingPriceCreateEvent");
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity entity = new HttpEntity<>(event, headers);
+
+ ResponseEntity response = restTemplate.exchange(
+ url, HttpMethod.POST, entity, String.class);
+
+ logger.info("Successfully sent product offering price create event to callback URL: {} - Response: {}",
+ url, response.getStatusCode());
+
+ } catch (Exception e) {
+ logger.error("Failed to send product offering price create event to callback URL: {}", callbackUrl, e);
+ }
+ }
+
+ /**
+ * Send product offering price delete event to a specific callback URL
+ * @param callbackUrl The callback URL to send to
+ * @param event The product offering price delete event
+ */
+ private void sendProductOfferingPriceDeleteEventToCallback(String callbackUrl, ProductOfferingPriceDeleteEvent event) {
+ try {
+ String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingPriceDeleteEvent");
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity entity = new HttpEntity<>(event, headers);
+
+ ResponseEntity response = restTemplate.exchange(
+ url, HttpMethod.POST, entity, String.class);
+
+ logger.info("Successfully sent product offering price delete event to callback URL: {} - Response: {}",
+ url, response.getStatusCode());
+
+ } catch (Exception e) {
+ logger.error("Failed to send product offering price delete event to callback URL: {}", callbackUrl, e);
+ }
+ }
+
+ /**
+ * Send product offering price attribute value change event to a specific callback URL
+ * @param callbackUrl The callback URL to send to
+ * @param event The product offering price attribute value change event
+ */
+ private void sendProductOfferingPriceAttributeValueChangeEventToCallback(String callbackUrl, ProductOfferingPriceAttributeValueChangeEvent event) {
+ try {
+ String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingPriceAttributeValueChangeEvent");
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity entity = new HttpEntity<>(event, headers);
+
+ ResponseEntity response = restTemplate.exchange(
+ url, HttpMethod.POST, entity, String.class);
+
+ logger.info("Successfully sent product offering price attribute value change event to callback URL: {} - Response: {}",
+ url, response.getStatusCode());
+
+ } catch (Exception e) {
+ logger.error("Failed to send product offering price attribute value change event to callback URL: {}", callbackUrl, e);
+ }
+ }
+
+ /**
+ * Send product offering price state change event to a specific callback URL
+ * @param callbackUrl The callback URL to send to
+ * @param event The product offering price state change event
+ */
+ private void sendProductOfferingPriceStateChangeEventToCallback(String callbackUrl, ProductOfferingPriceStateChangeEvent event) {
+ try {
+ String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingPriceStateChangeEvent");
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity entity = new HttpEntity<>(event, headers);
+
+ ResponseEntity response = restTemplate.exchange(
+ url, HttpMethod.POST, entity, String.class);
+
+ logger.info("Successfully sent product offering price state change event to callback URL: {} - Response: {}",
+ url, response.getStatusCode());
+
+ } catch (Exception e) {
+ logger.error("Failed to send product offering price state change event to callback URL: {}", callbackUrl, e);
+ }
+ }
+
+ /**
+ * Build the full callback URL with the listener endpoint
+ * @param baseUrl The base callback URL
+ * @param listenerPath The listener path to append
+ * @return The complete callback URL
+ */
+ private String buildCallbackUrl(String baseUrl, String listenerPath) {
+ if (baseUrl.endsWith("/")) {
+ return baseUrl.substring(0, baseUrl.length() - 1) + listenerPath;
+ } else {
+ return baseUrl + listenerPath;
+ }
+ }
+
+ /**
+ * Check if a subscription should be notified for a specific event type
+ * @param subscription The event subscription
+ * @param eventType The event type to check
+ * @return true if the subscription should be notified
+ */
+ private boolean shouldNotifySubscription(EventSubscription subscription, String eventType) {
+ // If no query is specified, notify all events
+ if (subscription.getQuery() == null || subscription.getQuery().trim().isEmpty()) {
+ return true;
+ }
+
+ // Check if the query contains the event type
+ String query = subscription.getQuery().toLowerCase();
+ return query.contains("productofferingprice") ||
+ query.contains(eventType.toLowerCase()) ||
+ query.contains("productofferingprice.create") ||
+ query.contains("productofferingprice.delete") ||
+ query.contains("productofferingprice.attributevaluechange") ||
+ query.contains("productofferingprice.statechange");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceNotificationService.java
new file mode 100644
index 0000000000000000000000000000000000000000..336c51e643942c42e37966e0418c57afa2ec59ad
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceNotificationService.java
@@ -0,0 +1,257 @@
+/*-
+ * ========================LICENSE_START=================================
+ * org.etsi.osl.tmf.api
+ * %%
+ * Copyright (C) 2019 - 2021 openslice.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+package org.etsi.osl.tmf.pcm620.reposervices;
+
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.UUID;
+
+import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPrice;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateEventPayload;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteEventPayload;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeEventPayload;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeEventPayload;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeNotification;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional
+public class ProductOfferingPriceNotificationService {
+
+ private static final Logger logger = LoggerFactory.getLogger(ProductOfferingPriceNotificationService.class);
+
+ @Autowired
+ private ProductCatalogApiRouteBuilderEvents eventPublisher;
+
+ @Autowired
+ private ProductOfferingPriceCallbackService productOfferingPriceCallbackService;
+
+ /**
+ * Publish a product offering price create notification
+ * @param productOfferingPrice The created product offering price
+ */
+ public void publishProductOfferingPriceCreateNotification(ProductOfferingPrice productOfferingPrice) {
+ try {
+ ProductOfferingPriceCreateNotification notification = createProductOfferingPriceCreateNotification(productOfferingPrice);
+ eventPublisher.publishEvent(notification, productOfferingPrice.getId());
+
+ // Send callbacks to registered subscribers
+ productOfferingPriceCallbackService.sendProductOfferingPriceCreateCallback(notification.getEvent());
+
+ logger.info("Published product offering price create notification for product offering price ID: {}", productOfferingPrice.getId());
+ } catch (Exception e) {
+ logger.error("Error publishing product offering price create notification for product offering price ID: {}", productOfferingPrice.getId(), e);
+ }
+ }
+
+ /**
+ * Publish a product offering price delete notification
+ * @param productOfferingPrice The deleted product offering price
+ */
+ public void publishProductOfferingPriceDeleteNotification(ProductOfferingPrice productOfferingPrice) {
+ try {
+ ProductOfferingPriceDeleteNotification notification = createProductOfferingPriceDeleteNotification(productOfferingPrice);
+ eventPublisher.publishEvent(notification, productOfferingPrice.getId());
+
+ // Send callbacks to registered subscribers
+ productOfferingPriceCallbackService.sendProductOfferingPriceDeleteCallback(notification.getEvent());
+
+ logger.info("Published product offering price delete notification for product offering price ID: {}", productOfferingPrice.getId());
+ } catch (Exception e) {
+ logger.error("Error publishing product offering price delete notification for product offering price ID: {}", productOfferingPrice.getId(), e);
+ }
+ }
+
+ /**
+ * Publish a product offering price attribute value change notification
+ * @param productOfferingPrice The product offering price with changed attributes
+ */
+ public void publishProductOfferingPriceAttributeValueChangeNotification(ProductOfferingPrice productOfferingPrice) {
+ try {
+ ProductOfferingPriceAttributeValueChangeNotification notification = createProductOfferingPriceAttributeValueChangeNotification(productOfferingPrice);
+ eventPublisher.publishEvent(notification, productOfferingPrice.getId());
+
+ // Send callbacks to registered subscribers
+ productOfferingPriceCallbackService.sendProductOfferingPriceAttributeValueChangeCallback(notification.getEvent());
+
+ logger.info("Published product offering price attribute value change notification for product offering price ID: {}", productOfferingPrice.getId());
+ } catch (Exception e) {
+ logger.error("Error publishing product offering price attribute value change notification for product offering price ID: {}", productOfferingPrice.getId(), e);
+ }
+ }
+
+ /**
+ * Publish a product offering price state change notification
+ * @param productOfferingPrice The product offering price with changed state
+ */
+ public void publishProductOfferingPriceStateChangeNotification(ProductOfferingPrice productOfferingPrice) {
+ try {
+ ProductOfferingPriceStateChangeNotification notification = createProductOfferingPriceStateChangeNotification(productOfferingPrice);
+ eventPublisher.publishEvent(notification, productOfferingPrice.getId());
+
+ // Send callbacks to registered subscribers
+ productOfferingPriceCallbackService.sendProductOfferingPriceStateChangeCallback(notification.getEvent());
+
+ logger.info("Published product offering price state change notification for product offering price ID: {}", productOfferingPrice.getId());
+ } catch (Exception e) {
+ logger.error("Error publishing product offering price state change notification for product offering price ID: {}", productOfferingPrice.getId(), e);
+ }
+ }
+
+ /**
+ * Create a product offering price create notification
+ * @param productOfferingPrice The created product offering price
+ * @return ProductOfferingPriceCreateNotification
+ */
+ private ProductOfferingPriceCreateNotification createProductOfferingPriceCreateNotification(ProductOfferingPrice productOfferingPrice) {
+ ProductOfferingPriceCreateNotification notification = new ProductOfferingPriceCreateNotification();
+
+ // Set common notification properties
+ notification.setEventId(UUID.randomUUID().toString());
+ notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC));
+ notification.setEventType(ProductOfferingPriceCreateNotification.class.getName());
+ notification.setResourcePath("/productCatalogManagement/v4/productOfferingPrice/" + productOfferingPrice.getId());
+
+ // Create event
+ ProductOfferingPriceCreateEvent event = new ProductOfferingPriceCreateEvent();
+ event.setEventId(notification.getEventId());
+ event.setEventTime(notification.getEventTime());
+ event.setEventType("ProductOfferingPriceCreateEvent");
+ event.setTitle("Product Offering Price Create Event");
+ event.setDescription("A product offering price has been created");
+ event.setTimeOcurred(notification.getEventTime());
+
+ // Create event payload
+ ProductOfferingPriceCreateEventPayload payload = new ProductOfferingPriceCreateEventPayload();
+ payload.setProductOfferingPrice(productOfferingPrice);
+ event.setEvent(payload);
+
+ notification.setEvent(event);
+ return notification;
+ }
+
+ /**
+ * Create a product offering price delete notification
+ * @param productOfferingPrice The deleted product offering price
+ * @return ProductOfferingPriceDeleteNotification
+ */
+ private ProductOfferingPriceDeleteNotification createProductOfferingPriceDeleteNotification(ProductOfferingPrice productOfferingPrice) {
+ ProductOfferingPriceDeleteNotification notification = new ProductOfferingPriceDeleteNotification();
+
+ // Set common notification properties
+ notification.setEventId(UUID.randomUUID().toString());
+ notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC));
+ notification.setEventType(ProductOfferingPriceDeleteNotification.class.getName());
+ notification.setResourcePath("/productCatalogManagement/v4/productOfferingPrice/" + productOfferingPrice.getId());
+
+ // Create event
+ ProductOfferingPriceDeleteEvent event = new ProductOfferingPriceDeleteEvent();
+ event.setEventId(notification.getEventId());
+ event.setEventTime(notification.getEventTime());
+ event.setEventType("ProductOfferingPriceDeleteEvent");
+ event.setTitle("Product Offering Price Delete Event");
+ event.setDescription("A product offering price has been deleted");
+ event.setTimeOcurred(notification.getEventTime());
+
+ // Create event payload
+ ProductOfferingPriceDeleteEventPayload payload = new ProductOfferingPriceDeleteEventPayload();
+ payload.setProductOfferingPrice(productOfferingPrice);
+ event.setEvent(payload);
+
+ notification.setEvent(event);
+ return notification;
+ }
+
+ /**
+ * Create a product offering price attribute value change notification
+ * @param productOfferingPrice The product offering price with changed attributes
+ * @return ProductOfferingPriceAttributeValueChangeNotification
+ */
+ private ProductOfferingPriceAttributeValueChangeNotification createProductOfferingPriceAttributeValueChangeNotification(ProductOfferingPrice productOfferingPrice) {
+ ProductOfferingPriceAttributeValueChangeNotification notification = new ProductOfferingPriceAttributeValueChangeNotification();
+
+ // Set common notification properties
+ notification.setEventId(UUID.randomUUID().toString());
+ notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC));
+ notification.setEventType(ProductOfferingPriceAttributeValueChangeNotification.class.getName());
+ notification.setResourcePath("/productCatalogManagement/v4/productOfferingPrice/" + productOfferingPrice.getId());
+
+ // Create event
+ ProductOfferingPriceAttributeValueChangeEvent event = new ProductOfferingPriceAttributeValueChangeEvent();
+ event.setEventId(notification.getEventId());
+ event.setEventTime(notification.getEventTime());
+ event.setEventType("ProductOfferingPriceAttributeValueChangeEvent");
+ event.setTitle("Product Offering Price Attribute Value Change Event");
+ event.setDescription("A product offering price attribute value has been changed");
+ event.setTimeOcurred(notification.getEventTime());
+
+ // Create event payload
+ ProductOfferingPriceAttributeValueChangeEventPayload payload = new ProductOfferingPriceAttributeValueChangeEventPayload();
+ payload.setProductOfferingPrice(productOfferingPrice);
+ event.setEvent(payload);
+
+ notification.setEvent(event);
+ return notification;
+ }
+
+ /**
+ * Create a product offering price state change notification
+ * @param productOfferingPrice The product offering price with changed state
+ * @return ProductOfferingPriceStateChangeNotification
+ */
+ private ProductOfferingPriceStateChangeNotification createProductOfferingPriceStateChangeNotification(ProductOfferingPrice productOfferingPrice) {
+ ProductOfferingPriceStateChangeNotification notification = new ProductOfferingPriceStateChangeNotification();
+
+ // Set common notification properties
+ notification.setEventId(UUID.randomUUID().toString());
+ notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC));
+ notification.setEventType(ProductOfferingPriceStateChangeNotification.class.getName());
+ notification.setResourcePath("/productCatalogManagement/v4/productOfferingPrice/" + productOfferingPrice.getId());
+
+ // Create event
+ ProductOfferingPriceStateChangeEvent event = new ProductOfferingPriceStateChangeEvent();
+ event.setEventId(notification.getEventId());
+ event.setEventTime(notification.getEventTime());
+ event.setEventType("ProductOfferingPriceStateChangeEvent");
+ event.setTitle("Product Offering Price State Change Event");
+ event.setDescription("A product offering price state has been changed");
+ event.setTimeOcurred(notification.getEventTime());
+
+ // Create event payload
+ ProductOfferingPriceStateChangeEventPayload payload = new ProductOfferingPriceStateChangeEventPayload();
+ payload.setProductOfferingPrice(productOfferingPrice);
+ event.setEvent(payload);
+
+ notification.setEvent(event);
+ return notification;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceRepoService.java
index bffff81ec013962bf143190de4dfc67911472caa..3ad540219a9413fe7ca9aa5ac05263851b90a721 100644
--- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceRepoService.java
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceRepoService.java
@@ -55,6 +55,9 @@ public class ProductOfferingPriceRepoService {
@Autowired
ProductOfferingPriceRepository prodsOfferingRepo;
+ @Autowired
+ ProductOfferingPriceNotificationService productOfferingPriceNotificationService;
+
private SessionFactory sessionFactory;
@@ -80,6 +83,10 @@ public class ProductOfferingPriceRepoService {
serviceSpec = this.updateProductOfferingPriceDataFromAPIcall(serviceSpec, serviceProductOfferingPrice);
serviceSpec = this.prodsOfferingRepo.save(serviceSpec);
+ // Publish create notification
+ if (productOfferingPriceNotificationService != null) {
+ productOfferingPriceNotificationService.publishProductOfferingPriceCreateNotification(serviceSpec);
+ }
return this.prodsOfferingRepo.save(serviceSpec);
}
@@ -231,6 +238,11 @@ public class ProductOfferingPriceRepoService {
* prior deleting we need to delete other dependency objects
*/
+ // Publish delete notification before actual deletion
+ if (productOfferingPriceNotificationService != null) {
+ productOfferingPriceNotificationService.publishProductOfferingPriceDeleteNotification(s);
+ }
+
this.prodsOfferingRepo.delete(s);
return null;
}
@@ -244,12 +256,25 @@ public class ProductOfferingPriceRepoService {
if (s == null) {
return null;
}
+
+ // Store original state for comparison
+ String originalLifecycleStatus = s.getLifecycleStatus();
+
ProductOfferingPrice prodOff = s;
prodOff = this.updateProductOfferingPriceDataFromAPIcall(prodOff, aProductOfferingPrice);
prodOff = this.prodsOfferingRepo.save(prodOff);
-
+ // Publish notifications
+ if (productOfferingPriceNotificationService != null) {
+ // Always publish attribute value change notification for updates
+ productOfferingPriceNotificationService.publishProductOfferingPriceAttributeValueChangeNotification(prodOff);
+
+ // Check for state change and publish state change notification if needed
+ if (originalLifecycleStatus != null && !originalLifecycleStatus.equals(prodOff.getLifecycleStatus())) {
+ productOfferingPriceNotificationService.publishProductOfferingPriceStateChangeNotification(prodOff);
+ }
+ }
return this.prodsOfferingRepo.save(prodOff);
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java
index 942d0e7d91331ea7958797bf88af6814d2996736..02b0d19f2ff13377b0b81419da1a0142ff213149 100644
--- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java
@@ -77,6 +77,9 @@ public class ProductOfferingRepoService {
@Autowired
ServiceSpecificationRepoService serviceSpecificationRepoService;
+
+ @Autowired
+ private ProductOfferingNotificationService productOfferingNotificationService;
private SessionFactory sessionFactory;
@@ -101,8 +104,12 @@ public class ProductOfferingRepoService {
serviceSpec = this.updateProductOfferingDataFromAPIcall(serviceSpec, serviceProductOffering);
serviceSpec = this.prodsOfferingRepo.save(serviceSpec);
+ // Publish product offering create notification
+ if (productOfferingNotificationService != null) {
+ productOfferingNotificationService.publishProductOfferingCreateNotification(serviceSpec);
+ }
- return this.prodsOfferingRepo.save(serviceSpec);
+ return serviceSpec;
}
public List findAll() {
@@ -259,8 +266,14 @@ public class ProductOfferingRepoService {
/**
* prior deleting we need to delete other dependency objects
*/
+
+ // Publish product offering delete notification BEFORE deletion to ensure session is still active
+ if (productOfferingNotificationService != null) {
+ productOfferingNotificationService.publishProductOfferingDeleteNotification(s);
+ }
this.prodsOfferingRepo.delete(s);
+
return null;
}
@@ -273,14 +286,28 @@ public class ProductOfferingRepoService {
if (s == null) {
return null;
}
+
+ // Store original state for comparison
+ String originalLifecycleStatus = s.getLifecycleStatus();
+
ProductOffering prodOff = s;
prodOff = this.updateProductOfferingDataFromAPIcall(prodOff, aProductOffering);
prodOff = this.prodsOfferingRepo.save(prodOff);
+ // Publish notifications
+ if (productOfferingNotificationService != null) {
+ // Always publish attribute value change notification on update
+ productOfferingNotificationService.publishProductOfferingAttributeValueChangeNotification(prodOff);
+
+ // Publish state change notification if lifecycle status changed
+ if (originalLifecycleStatus != null && prodOff.getLifecycleStatus() != null
+ && !originalLifecycleStatus.equals(prodOff.getLifecycleStatus())) {
+ productOfferingNotificationService.publishProductOfferingStateChangeNotification(prodOff);
+ }
+ }
-
- return this.prodsOfferingRepo.save(prodOff);
+ return prodOff;
}
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationCallbackService.java
new file mode 100644
index 0000000000000000000000000000000000000000..ae6271912ffe06b31d9faa558ace25e2dda1eb4e
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationCallbackService.java
@@ -0,0 +1,158 @@
+/*-
+ * ========================LICENSE_START=================================
+ * org.etsi.osl.tmf.api
+ * %%
+ * Copyright (C) 2019 - 2021 openslice.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+package org.etsi.osl.tmf.pcm620.reposervices;
+
+import java.util.List;
+
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.EventSubscription;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+@Service
+public class ProductSpecificationCallbackService {
+
+ private static final Logger logger = LoggerFactory.getLogger(ProductSpecificationCallbackService.class);
+
+ @Autowired
+ private EventSubscriptionRepoService eventSubscriptionRepoService;
+
+ @Autowired
+ private RestTemplate restTemplate;
+
+ /**
+ * Send product specification create event to all registered callback URLs
+ * @param productSpecificationCreateEvent The product specification create event to send
+ */
+ public void sendProductSpecificationCreateCallback(ProductSpecificationCreateEvent productSpecificationCreateEvent) {
+ List subscriptions = eventSubscriptionRepoService.findAll();
+
+ for (EventSubscription subscription : subscriptions) {
+ if (shouldNotifySubscription(subscription, "productSpecificationCreateEvent")) {
+ sendProductSpecificationCreateEventToCallback(subscription.getCallback(), productSpecificationCreateEvent);
+ }
+ }
+ }
+
+ /**
+ * Send product specification delete event to all registered callback URLs
+ * @param productSpecificationDeleteEvent The product specification delete event to send
+ */
+ public void sendProductSpecificationDeleteCallback(ProductSpecificationDeleteEvent productSpecificationDeleteEvent) {
+ List subscriptions = eventSubscriptionRepoService.findAll();
+
+ for (EventSubscription subscription : subscriptions) {
+ if (shouldNotifySubscription(subscription, "productSpecificationDeleteEvent")) {
+ sendProductSpecificationDeleteEventToCallback(subscription.getCallback(), productSpecificationDeleteEvent);
+ }
+ }
+ }
+
+ /**
+ * Send product specification create event to a specific callback URL
+ * @param callbackUrl The callback URL to send to
+ * @param event The product specification create event
+ */
+ private void sendProductSpecificationCreateEventToCallback(String callbackUrl, ProductSpecificationCreateEvent event) {
+ try {
+ String url = buildCallbackUrl(callbackUrl, "/listener/productSpecificationCreateEvent");
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity entity = new HttpEntity<>(event, headers);
+
+ ResponseEntity response = restTemplate.exchange(
+ url, HttpMethod.POST, entity, String.class);
+
+ logger.info("Successfully sent product specification create event to callback URL: {} - Response: {}",
+ url, response.getStatusCode());
+
+ } catch (Exception e) {
+ logger.error("Failed to send product specification create event to callback URL: {}", callbackUrl, e);
+ }
+ }
+
+ /**
+ * Send product specification delete event to a specific callback URL
+ * @param callbackUrl The callback URL to send to
+ * @param event The product specification delete event
+ */
+ private void sendProductSpecificationDeleteEventToCallback(String callbackUrl, ProductSpecificationDeleteEvent event) {
+ try {
+ String url = buildCallbackUrl(callbackUrl, "/listener/productSpecificationDeleteEvent");
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity entity = new HttpEntity<>(event, headers);
+
+ ResponseEntity response = restTemplate.exchange(
+ url, HttpMethod.POST, entity, String.class);
+
+ logger.info("Successfully sent product specification delete event to callback URL: {} - Response: {}",
+ url, response.getStatusCode());
+
+ } catch (Exception e) {
+ logger.error("Failed to send product specification delete event to callback URL: {}", callbackUrl, e);
+ }
+ }
+
+ /**
+ * Build the full callback URL with the listener endpoint
+ * @param baseUrl The base callback URL
+ * @param listenerPath The listener path to append
+ * @return The complete callback URL
+ */
+ private String buildCallbackUrl(String baseUrl, String listenerPath) {
+ if (baseUrl.endsWith("/")) {
+ return baseUrl.substring(0, baseUrl.length() - 1) + listenerPath;
+ } else {
+ return baseUrl + listenerPath;
+ }
+ }
+
+ /**
+ * Check if a subscription should be notified for a specific event type
+ * @param subscription The event subscription
+ * @param eventType The event type to check
+ * @return true if the subscription should be notified
+ */
+ private boolean shouldNotifySubscription(EventSubscription subscription, String eventType) {
+ // If no query is specified, notify all events
+ if (subscription.getQuery() == null || subscription.getQuery().trim().isEmpty()) {
+ return true;
+ }
+
+ // Check if the query contains the event type
+ String query = subscription.getQuery().toLowerCase();
+ return query.contains("productspecification") ||
+ query.contains(eventType.toLowerCase()) ||
+ query.contains("productspecification.create") ||
+ query.contains("productspecification.delete");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationNotificationService.java
new file mode 100644
index 0000000000000000000000000000000000000000..0818539a7fcd92886e10f19547b40b62b6a6aaeb
--- /dev/null
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationNotificationService.java
@@ -0,0 +1,151 @@
+/*-
+ * ========================LICENSE_START=================================
+ * org.etsi.osl.tmf.api
+ * %%
+ * Copyright (C) 2019 - 2021 openslice.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================LICENSE_END==================================
+ */
+package org.etsi.osl.tmf.pcm620.reposervices;
+
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.UUID;
+
+import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecification;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateEventPayload;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteEventPayload;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteNotification;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional
+public class ProductSpecificationNotificationService {
+
+ private static final Logger logger = LoggerFactory.getLogger(ProductSpecificationNotificationService.class);
+
+ @Autowired
+ private ProductCatalogApiRouteBuilderEvents eventPublisher;
+
+ @Autowired
+ private ProductSpecificationCallbackService productSpecificationCallbackService;
+
+ /**
+ * Publish a product specification create notification
+ * @param productSpecification The created product specification
+ */
+ public void publishProductSpecificationCreateNotification(ProductSpecification productSpecification) {
+ try {
+ ProductSpecificationCreateNotification notification = createProductSpecificationCreateNotification(productSpecification);
+ eventPublisher.publishEvent(notification, productSpecification.getUuid());
+
+ // Send callbacks to registered subscribers
+ productSpecificationCallbackService.sendProductSpecificationCreateCallback(notification.getEvent());
+
+ logger.info("Published product specification create notification for product spec ID: {}", productSpecification.getUuid());
+ } catch (Exception e) {
+ logger.error("Error publishing product specification create notification for product spec ID: {}", productSpecification.getUuid(), e);
+ }
+ }
+
+ /**
+ * Publish a product specification delete notification
+ * @param productSpecification The deleted product specification
+ */
+ public void publishProductSpecificationDeleteNotification(ProductSpecification productSpecification) {
+ try {
+ ProductSpecificationDeleteNotification notification = createProductSpecificationDeleteNotification(productSpecification);
+ eventPublisher.publishEvent(notification, productSpecification.getUuid());
+
+ // Send callbacks to registered subscribers
+ productSpecificationCallbackService.sendProductSpecificationDeleteCallback(notification.getEvent());
+
+ logger.info("Published product specification delete notification for product spec ID: {}", productSpecification.getUuid());
+ } catch (Exception e) {
+ logger.error("Error publishing product specification delete notification for product spec ID: {}", productSpecification.getUuid(), e);
+ }
+ }
+
+ /**
+ * Create a product specification create notification
+ * @param productSpecification The created product specification
+ * @return ProductSpecificationCreateNotification
+ */
+ private ProductSpecificationCreateNotification createProductSpecificationCreateNotification(ProductSpecification productSpecification) {
+ ProductSpecificationCreateNotification notification = new ProductSpecificationCreateNotification();
+
+ // Set common notification properties
+ notification.setEventId(UUID.randomUUID().toString());
+ notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC));
+ notification.setEventType(ProductSpecificationCreateNotification.class.getName());
+ notification.setResourcePath("/productCatalogManagement/v4/productSpecification/" + productSpecification.getUuid());
+
+ // Create event
+ ProductSpecificationCreateEvent event = new ProductSpecificationCreateEvent();
+ event.setEventId(notification.getEventId());
+ event.setEventTime(notification.getEventTime());
+ event.setEventType("ProductSpecificationCreateEvent");
+ event.setTitle("Product Specification Create Event");
+ event.setDescription("A product specification has been created");
+ event.setTimeOcurred(notification.getEventTime());
+
+ // Create event payload
+ ProductSpecificationCreateEventPayload payload = new ProductSpecificationCreateEventPayload();
+ payload.setProductSpecification(productSpecification);
+ event.setEvent(payload);
+
+ notification.setEvent(event);
+ return notification;
+ }
+
+ /**
+ * Create a product specification delete notification
+ * @param productSpecification The deleted product specification
+ * @return ProductSpecificationDeleteNotification
+ */
+ private ProductSpecificationDeleteNotification createProductSpecificationDeleteNotification(ProductSpecification productSpecification) {
+ ProductSpecificationDeleteNotification notification = new ProductSpecificationDeleteNotification();
+
+ // Set common notification properties
+ notification.setEventId(UUID.randomUUID().toString());
+ notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC));
+ notification.setEventType(ProductSpecificationDeleteNotification.class.getName());
+ notification.setResourcePath("/productCatalogManagement/v4/productSpecification/" + productSpecification.getUuid());
+
+ // Create event
+ ProductSpecificationDeleteEvent event = new ProductSpecificationDeleteEvent();
+ event.setEventId(notification.getEventId());
+ event.setEventTime(notification.getEventTime());
+ event.setEventType("ProductSpecificationDeleteEvent");
+ event.setTitle("Product Specification Delete Event");
+ event.setDescription("A product specification has been deleted");
+ event.setTimeOcurred(notification.getEventTime());
+
+ // Create event payload
+ ProductSpecificationDeleteEventPayload payload = new ProductSpecificationDeleteEventPayload();
+ payload.setProductSpecification(productSpecification);
+ event.setEvent(payload);
+
+ notification.setEvent(event);
+ return notification;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java
index deec0a663436f017e7d66f89389fb22bd6cd8ea6..ab2e3ee3bf709274afb6c897c190567c1b51a045 100644
--- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java
+++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java
@@ -70,6 +70,9 @@ public class ProductSpecificationRepoService {
@Autowired
ServiceSpecificationRepoService serviceSpecificationRepoService;
+
+ @Autowired
+ private ProductSpecificationNotificationService productSpecificationNotificationService;
private SessionFactory sessionFactory;
@@ -94,8 +97,12 @@ public class ProductSpecificationRepoService {
serviceSpec = this.updateProductSpecificationDataFromAPIcall(serviceSpec, serviceProductSpecification);
serviceSpec = this.prodsOfferingRepo.save(serviceSpec);
+ // Publish product specification create notification
+ if (productSpecificationNotificationService != null) {
+ productSpecificationNotificationService.publishProductSpecificationCreateNotification(serviceSpec);
+ }
- return this.prodsOfferingRepo.save(serviceSpec);
+ return serviceSpec;
}
public List findAll() {
@@ -250,8 +257,14 @@ public class ProductSpecificationRepoService {
/**
* prior deleting we need to delete other dependency objects
*/
+
+ // Publish product specification delete notification BEFORE deletion to ensure session is still active
+ if (productSpecificationNotificationService != null) {
+ productSpecificationNotificationService.publishProductSpecificationDeleteNotification(s);
+ }
this.prodsOfferingRepo.delete(s);
+
return null;
}
diff --git a/src/main/resources/application-testing.yml b/src/main/resources/application-testing.yml
index dc15a9c271e512228cced92f454f39da56b09f55..1c08e30a03457f8f454adc1b3717c9719fd5d2be 100644
--- a/src/main/resources/application-testing.yml
+++ b/src/main/resources/application-testing.yml
@@ -185,6 +185,21 @@ EVENT_PRODUCT_ORDER_STATE_CHANGED: "jms:topic:EVENT.PRODUCTORDER.STATECHANGED"
EVENT_PRODUCT_ORDER_DELETE: "jms:topic:EVENT.PRODUCTORDER.DELETE"
EVENT_PRODUCT_ORDER_ATTRIBUTE_VALUE_CHANGED: "jms:topic:EVENT.PRODUCTORDER.ATTRCHANGED"
+EVENT_PRODUCT_CATALOG_CREATE: "jms:topic:EVENT.PRODUCTCATALOG.CREATE"
+EVENT_PRODUCT_CATALOG_DELETE: "jms:topic:EVENT.PRODUCTCATALOG.DELETE"
+EVENT_PRODUCT_CATEGORY_CREATE: "jms:topic:EVENT.PRODUCTCATEGORY.CREATE"
+EVENT_PRODUCT_CATEGORY_DELETE: "jms:topic:EVENT.PRODUCTCATEGORY.DELETE"
+EVENT_PRODUCT_SPECIFICATION_CREATE: "jms:topic:EVENT.PRODUCTSPECIFICATION.CREATE"
+EVENT_PRODUCT_SPECIFICATION_DELETE: "jms:topic:EVENT.PRODUCTSPECIFICATION.DELETE"
+EVENT_PRODUCT_OFFERING_CREATE: "jms:topic:EVENT.PRODUCTOFFERING.CREATE"
+EVENT_PRODUCT_OFFERING_DELETE: "jms:topic:EVENT.PRODUCTOFFERING.DELETE"
+EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.ATTRCHANGED"
+EVENT_PRODUCT_OFFERING_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.STATECHANGED"
+EVENT_PRODUCT_OFFERING_PRICE_CREATE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.CREATE"
+EVENT_PRODUCT_OFFERING_PRICE_DELETE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.DELETE"
+EVENT_PRODUCT_OFFERING_PRICE_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.ATTRCHANGED"
+EVENT_PRODUCT_OFFERING_PRICE_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.STATECHANGED"
+
#QUEUE MESSSAGES WITH VNFNSD CATALOG
NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID"
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index dc4f0ce805b2dccea5a1762bd78947b7093795be..2cf3438ac025cecde55ad09c2d732baa953eebcb 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -210,6 +210,21 @@ EVENT_PRODUCT_ORDER_STATE_CHANGED: "jms:topic:EVENT.PRODUCTORDER.STATECHANGED"
EVENT_PRODUCT_ORDER_DELETE: "jms:topic:EVENT.PRODUCTORDER.DELETE"
EVENT_PRODUCT_ORDER_ATTRIBUTE_VALUE_CHANGED: "jms:topic:EVENT.PRODUCTORDER.ATTRCHANGED"
+EVENT_PRODUCT_CATALOG_CREATE: "jms:topic:EVENT.PRODUCTCATALOG.CREATE"
+EVENT_PRODUCT_CATALOG_DELETE: "jms:topic:EVENT.PRODUCTCATALOG.DELETE"
+EVENT_PRODUCT_CATEGORY_CREATE: "jms:topic:EVENT.PRODUCTCATEGORY.CREATE"
+EVENT_PRODUCT_CATEGORY_DELETE: "jms:topic:EVENT.PRODUCTCATEGORY.DELETE"
+EVENT_PRODUCT_SPECIFICATION_CREATE: "jms:topic:EVENT.PRODUCTSPECIFICATION.CREATE"
+EVENT_PRODUCT_SPECIFICATION_DELETE: "jms:topic:EVENT.PRODUCTSPECIFICATION.DELETE"
+EVENT_PRODUCT_OFFERING_CREATE: "jms:topic:EVENT.PRODUCTOFFERING.CREATE"
+EVENT_PRODUCT_OFFERING_DELETE: "jms:topic:EVENT.PRODUCTOFFERING.DELETE"
+EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.ATTRCHANGED"
+EVENT_PRODUCT_OFFERING_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.STATECHANGED"
+EVENT_PRODUCT_OFFERING_PRICE_CREATE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.CREATE"
+EVENT_PRODUCT_OFFERING_PRICE_DELETE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.DELETE"
+EVENT_PRODUCT_OFFERING_PRICE_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.ATTRCHANGED"
+EVENT_PRODUCT_OFFERING_PRICE_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.STATECHANGED"
+
#QUEUE MESSSAGES WITH VNFNSD CATALOG
NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID"
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..0764364cc94fcc015122c4117082b6f40528b5fc
--- /dev/null
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java
@@ -0,0 +1,174 @@
+package org.etsi.osl.services.api.pcm620;
+
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.etsi.osl.tmf.JsonUtils;
+import org.etsi.osl.tmf.OpenAPISpringBoot;
+import org.etsi.osl.tmf.pcm620.model.Catalog;
+import org.etsi.osl.tmf.pcm620.model.CatalogCreate;
+import org.etsi.osl.tmf.pcm620.model.EventSubscription;
+import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput;
+import org.etsi.osl.tmf.pcm620.reposervices.CatalogCallbackService;
+import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductCatalogRepoService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.context.WebApplicationContext;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+@RunWith(SpringRunner.class)
+@Transactional
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class)
+@AutoConfigureMockMvc
+@ActiveProfiles("testing")
+@AutoConfigureTestDatabase
+public class CatalogCallbackIntegrationTest {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @Autowired
+ private WebApplicationContext context;
+
+ @Autowired
+ private ProductCatalogRepoService productCatalogRepoService;
+
+ @Autowired
+ private EventSubscriptionRepoService eventSubscriptionRepoService;
+
+ @SpyBean
+ private CatalogCallbackService catalogCallbackService;
+
+ @MockBean
+ private RestTemplate restTemplate;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ mvc = MockMvcBuilders
+ .webAppContextSetup(context)
+ .apply(springSecurity())
+ .build();
+
+ // Mock RestTemplate to avoid actual HTTP calls in tests
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK));
+ }
+
+ @Test
+ @WithMockUser(username = "osadmin", roles = {"ADMIN"})
+ public void testCompleteCallbackFlow() throws Exception {
+ // Step 1: Register a callback subscription via Hub API
+ EventSubscriptionInput subscriptionInput = new EventSubscriptionInput();
+ subscriptionInput.setCallback("http://localhost:8080/test-callback");
+ subscriptionInput.setQuery("catalog.create,catalog.delete");
+
+ MvcResult subscriptionResult = mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
+ .with(SecurityMockMvcRequestPostProcessors.csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(JsonUtils.toJson(subscriptionInput)))
+ .andExpect(status().isCreated())
+ .andReturn();
+
+ String subscriptionResponseBody = subscriptionResult.getResponse().getContentAsString();
+ EventSubscription createdSubscription = objectMapper.readValue(subscriptionResponseBody, EventSubscription.class);
+
+ // Step 2: Create a catalog (should trigger callback)
+ CatalogCreate catalogCreate = new CatalogCreate();
+ catalogCreate.setName("Test Callback Catalog");
+ catalogCreate.setDescription("A catalog to test callback notifications");
+ catalogCreate.setVersion("1.0");
+
+ Catalog createdCatalog = productCatalogRepoService.addCatalog(catalogCreate);
+
+ // Step 3: Verify callback was sent
+ verify(catalogCallbackService, timeout(2000)).sendCatalogCreateCallback(any());
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:8080/test-callback/listener/catalogCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+
+ // Step 4: Delete the catalog (should trigger delete callback)
+ productCatalogRepoService.deleteById(createdCatalog.getUuid());
+
+ // Step 5: Verify delete callback was sent
+ verify(catalogCallbackService, timeout(2000)).sendCatalogDeleteCallback(any());
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:8080/test-callback/listener/catalogDeleteEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+ }
+
+ @Test
+ @WithMockUser(username = "osadmin", roles = {"ADMIN"})
+ public void testCallbackFilteringByQuery() throws Exception {
+ // Step 1: Register subscription only for create events
+ EventSubscriptionInput subscriptionInput = new EventSubscriptionInput();
+ subscriptionInput.setCallback("http://localhost:9090/create-only");
+ subscriptionInput.setQuery("catalog.create");
+
+ mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
+ .with(SecurityMockMvcRequestPostProcessors.csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(JsonUtils.toJson(subscriptionInput)))
+ .andExpect(status().isCreated());
+
+ // Step 2: Create and delete a catalog
+ CatalogCreate catalogCreate = new CatalogCreate();
+ catalogCreate.setName("Test Filter Catalog");
+ catalogCreate.setDescription("A catalog to test query filtering");
+ catalogCreate.setVersion("1.0");
+
+ Catalog createdCatalog = productCatalogRepoService.addCatalog(catalogCreate);
+ productCatalogRepoService.deleteById(createdCatalog.getUuid());
+
+ // Step 3: Verify only create callback was sent (not delete)
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:9090/create-only/listener/catalogCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+
+ // Note: In a more sophisticated test, we could verify that the delete callback was NOT sent
+ // by using verify with never(), but this requires more complex mock setup
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..05312420d20c0efe7b63771305dbd70035fa84e3
--- /dev/null
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackServiceTest.java
@@ -0,0 +1,162 @@
+package org.etsi.osl.services.api.pcm620;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.etsi.osl.tmf.pcm620.model.Catalog;
+import org.etsi.osl.tmf.pcm620.model.CatalogCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.CatalogCreateEventPayload;
+import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEventPayload;
+import org.etsi.osl.tmf.pcm620.model.EventSubscription;
+import org.etsi.osl.tmf.pcm620.reposervices.CatalogCallbackService;
+import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.web.client.RestTemplate;
+
+@RunWith(SpringRunner.class)
+@ActiveProfiles("testing")
+public class CatalogCallbackServiceTest {
+
+ @Mock
+ private EventSubscriptionRepoService eventSubscriptionRepoService;
+
+ @Mock
+ private RestTemplate restTemplate;
+
+ @InjectMocks
+ private CatalogCallbackService catalogCallbackService;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ public void testSendCatalogCreateCallback() {
+ // Arrange
+ EventSubscription subscription1 = createSubscription("1", "http://localhost:8080/callback", "catalog.create");
+ EventSubscription subscription2 = createSubscription("2", "http://localhost:9090/webhook", "catalog");
+ List subscriptions = Arrays.asList(subscription1, subscription2);
+
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK));
+
+ CatalogCreateEvent event = createCatalogCreateEvent();
+
+ // Act
+ catalogCallbackService.sendCatalogCreateCallback(event);
+
+ // Assert
+ verify(restTemplate, times(2)).exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class));
+ }
+
+ @Test
+ public void testSendCatalogDeleteCallback() {
+ // Arrange
+ EventSubscription subscription = createSubscription("1", "http://localhost:8080/callback", "catalog.delete");
+ List subscriptions = Arrays.asList(subscription);
+
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK));
+
+ CatalogDeleteEvent event = createCatalogDeleteEvent();
+
+ // Act
+ catalogCallbackService.sendCatalogDeleteCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class));
+ }
+
+ @Test
+ public void testCallbackUrlBuilding() {
+ // Arrange
+ EventSubscription subscription1 = createSubscription("1", "http://localhost:8080/callback", "catalog");
+ EventSubscription subscription2 = createSubscription("2", "http://localhost:8080/callback/", "catalog");
+ List subscriptions = Arrays.asList(subscription1, subscription2);
+
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK));
+
+ CatalogCreateEvent event = createCatalogCreateEvent();
+
+ // Act
+ catalogCallbackService.sendCatalogCreateCallback(event);
+
+ // Assert - Both should result in the same URL format
+ verify(restTemplate, times(2)).exchange(eq("http://localhost:8080/callback/listener/catalogCreateEvent"),
+ eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class));
+ }
+
+ @Test
+ public void testNoSubscriptions() {
+ // Arrange
+ when(eventSubscriptionRepoService.findAll()).thenReturn(Arrays.asList());
+ CatalogCreateEvent event = createCatalogCreateEvent();
+
+ // Act
+ catalogCallbackService.sendCatalogCreateCallback(event);
+
+ // Assert - No calls should be made
+ verify(restTemplate, times(0)).exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class));
+ }
+
+ private EventSubscription createSubscription(String id, String callback, String query) {
+ EventSubscription subscription = new EventSubscription();
+ subscription.setId(id);
+ subscription.setCallback(callback);
+ subscription.setQuery(query);
+ return subscription;
+ }
+
+ private CatalogCreateEvent createCatalogCreateEvent() {
+ CatalogCreateEvent event = new CatalogCreateEvent();
+ event.setEventId("test-event-123");
+ event.setEventType("CatalogCreateEvent");
+
+ CatalogCreateEventPayload payload = new CatalogCreateEventPayload();
+ Catalog catalog = new Catalog();
+ catalog.setUuid("test-catalog-123");
+ catalog.setName("Test Catalog");
+ payload.setCatalog(catalog);
+
+ event.setEvent(payload);
+ return event;
+ }
+
+ private CatalogDeleteEvent createCatalogDeleteEvent() {
+ CatalogDeleteEvent event = new CatalogDeleteEvent();
+ event.setEventId("test-delete-event-123");
+ event.setEventType("CatalogDeleteEvent");
+
+ CatalogDeleteEventPayload payload = new CatalogDeleteEventPayload();
+ Catalog catalog = new Catalog();
+ catalog.setUuid("test-catalog-123");
+ catalog.setName("Test Catalog");
+ payload.setCatalog(catalog);
+
+ event.setEvent(payload);
+ return event;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..c5fa4406fa9ba053423bc7f9ddfb9489289bf80c
--- /dev/null
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationIntegrationTest.java
@@ -0,0 +1,118 @@
+package org.etsi.osl.services.api.pcm620;
+
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.etsi.osl.tmf.OpenAPISpringBoot;
+import org.etsi.osl.tmf.pcm620.model.Catalog;
+import org.etsi.osl.tmf.pcm620.model.CatalogCreate;
+import org.etsi.osl.tmf.pcm620.reposervices.CatalogNotificationService;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductCatalogRepoService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.context.WebApplicationContext;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+@RunWith(SpringRunner.class)
+@Transactional
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class)
+@AutoConfigureMockMvc
+@ActiveProfiles("testing")
+@AutoConfigureTestDatabase
+public class CatalogNotificationIntegrationTest {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @Autowired
+ private WebApplicationContext context;
+
+ @Autowired
+ private ProductCatalogRepoService productCatalogRepoService;
+
+ @SpyBean
+ private CatalogNotificationService catalogNotificationService;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ mvc = MockMvcBuilders
+ .webAppContextSetup(context)
+ .apply(springSecurity())
+ .build();
+ }
+
+ @Test
+ @WithMockUser(username = "osadmin", roles = {"ADMIN"})
+ public void testCatalogCreateNotificationFlow() throws Exception {
+ // Arrange
+ CatalogCreate catalogCreate = new CatalogCreate();
+ catalogCreate.setName("Test Notification Catalog");
+ catalogCreate.setDescription("A catalog to test notifications");
+ catalogCreate.setVersion("1.0");
+
+ // Act - Create catalog through repository service
+ Catalog createdCatalog = productCatalogRepoService.addCatalog(catalogCreate);
+
+ // Assert - Verify notification was published
+ verify(catalogNotificationService, timeout(1000)).publishCatalogCreateNotification(any(Catalog.class));
+
+ // Verify catalog was created
+ assert createdCatalog != null;
+ assert createdCatalog.getName().equals("Test Notification Catalog");
+ assert createdCatalog.getUuid() != null;
+ }
+
+ @Test
+ @WithMockUser(username = "osadmin", roles = {"ADMIN"})
+ public void testCatalogDeleteNotificationFlow() throws Exception {
+ // Arrange - First create a catalog
+ CatalogCreate catalogCreate = new CatalogCreate();
+ catalogCreate.setName("Test Delete Notification Catalog");
+ catalogCreate.setDescription("A catalog to test delete notifications");
+ catalogCreate.setVersion("1.0");
+
+ Catalog createdCatalog = productCatalogRepoService.addCatalog(catalogCreate);
+ String catalogId = createdCatalog.getUuid();
+
+ // Act - Delete the catalog
+ productCatalogRepoService.deleteById(catalogId);
+
+ // Assert - Verify both create and delete notifications were published
+ verify(catalogNotificationService, timeout(1000)).publishCatalogCreateNotification(any(Catalog.class));
+ verify(catalogNotificationService, timeout(1000)).publishCatalogDeleteNotification(any(Catalog.class));
+ }
+
+ @Test
+ public void testDirectCatalogOperations() {
+ // Arrange
+ Catalog catalog = new Catalog();
+ catalog.setName("Direct Test Catalog");
+ catalog.setDescription("Direct catalog for testing");
+
+ // Act - Add catalog directly
+ Catalog savedCatalog = productCatalogRepoService.addCatalog(catalog);
+
+ // Assert - Verify notification was called
+ verify(catalogNotificationService, timeout(1000)).publishCatalogCreateNotification(any(Catalog.class));
+
+ assert savedCatalog != null;
+ assert savedCatalog.getName().equals("Direct Test Catalog");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2a4c666ea0bf6ea2aae43afbce200752bf4fcb0e
--- /dev/null
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java
@@ -0,0 +1,84 @@
+package org.etsi.osl.services.api.pcm620;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import org.etsi.osl.tmf.OpenAPISpringBoot;
+import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents;
+import org.etsi.osl.tmf.pcm620.model.Catalog;
+import org.etsi.osl.tmf.pcm620.model.CatalogCreateNotification;
+import org.etsi.osl.tmf.pcm620.model.CatalogDeleteNotification;
+import org.etsi.osl.tmf.pcm620.reposervices.CatalogNotificationService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class)
+@ActiveProfiles("testing")
+@AutoConfigureMockMvc
+public class CatalogNotificationServiceTest {
+
+ @Mock
+ private ProductCatalogApiRouteBuilderEvents eventPublisher;
+
+ @InjectMocks
+ private CatalogNotificationService catalogNotificationService;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ public void testPublishCatalogCreateNotification() {
+ // Arrange
+ Catalog catalog = new Catalog();
+ catalog.setUuid("test-catalog-123");
+ catalog.setName("Test Catalog");
+ catalog.setDescription("A test catalog for notifications");
+
+ // Act
+ catalogNotificationService.publishCatalogCreateNotification(catalog);
+
+ // Assert
+ verify(eventPublisher, times(1)).publishEvent(any(CatalogCreateNotification.class), eq("test-catalog-123"));
+ }
+
+ @Test
+ public void testPublishCatalogDeleteNotification() {
+ // Arrange
+ Catalog catalog = new Catalog();
+ catalog.setUuid("test-catalog-456");
+ catalog.setName("Test Catalog to Delete");
+ catalog.setDescription("A test catalog for delete notifications");
+
+ // Act
+ catalogNotificationService.publishCatalogDeleteNotification(catalog);
+
+ // Assert
+ verify(eventPublisher, times(1)).publishEvent(any(CatalogDeleteNotification.class), eq("test-catalog-456"));
+ }
+
+ @Test
+ public void testCreateNotificationStructure() {
+ // Arrange
+ Catalog catalog = new Catalog();
+ catalog.setUuid("test-catalog-789");
+ catalog.setName("Test Catalog Structure");
+
+ // Act
+ catalogNotificationService.publishCatalogCreateNotification(catalog);
+
+ // Assert - verify the notification was published with correct structure
+ verify(eventPublisher).publishEvent(any(CatalogCreateNotification.class), eq("test-catalog-789"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a694ae4639ef7750f325bced0f4616d5c6013bbe
--- /dev/null
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackIntegrationTest.java
@@ -0,0 +1,205 @@
+package org.etsi.osl.services.api.pcm620;
+
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.etsi.osl.tmf.JsonUtils;
+import org.etsi.osl.tmf.OpenAPISpringBoot;
+import org.etsi.osl.tmf.pcm620.model.Category;
+import org.etsi.osl.tmf.pcm620.model.CategoryCreate;
+import org.etsi.osl.tmf.pcm620.model.EventSubscription;
+import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput;
+import org.etsi.osl.tmf.pcm620.reposervices.CategoryCallbackService;
+import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductCategoryRepoService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.context.WebApplicationContext;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+@RunWith(SpringRunner.class)
+@Transactional
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class)
+@AutoConfigureMockMvc
+@ActiveProfiles("testing")
+@AutoConfigureTestDatabase
+public class CategoryCallbackIntegrationTest {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @Autowired
+ private WebApplicationContext context;
+
+ @Autowired
+ private ProductCategoryRepoService productCategoryRepoService;
+
+ @Autowired
+ private EventSubscriptionRepoService eventSubscriptionRepoService;
+
+ @SpyBean
+ private CategoryCallbackService categoryCallbackService;
+
+ @MockBean
+ private RestTemplate restTemplate;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ mvc = MockMvcBuilders
+ .webAppContextSetup(context)
+ .apply(springSecurity())
+ .build();
+
+ // Mock RestTemplate to avoid actual HTTP calls in tests
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK));
+ }
+
+ @Test
+ @WithMockUser(username = "osadmin", roles = {"ADMIN"})
+ public void testCompleteCallbackFlow() throws Exception {
+ // Step 1: Register a callback subscription via Hub API
+ EventSubscriptionInput subscriptionInput = new EventSubscriptionInput();
+ subscriptionInput.setCallback("http://localhost:8080/test-callback");
+ subscriptionInput.setQuery("category.create,category.delete");
+
+ MvcResult subscriptionResult = mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
+ .with(SecurityMockMvcRequestPostProcessors.csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(JsonUtils.toJson(subscriptionInput)))
+ .andExpect(status().isCreated())
+ .andReturn();
+
+ String subscriptionResponseBody = subscriptionResult.getResponse().getContentAsString();
+ EventSubscription createdSubscription = objectMapper.readValue(subscriptionResponseBody, EventSubscription.class);
+
+ // Step 2: Create a category (should trigger callback)
+ CategoryCreate categoryCreate = new CategoryCreate();
+ categoryCreate.setName("Test Callback Category");
+ categoryCreate.setDescription("A category to test callback notifications");
+ categoryCreate.setVersion("1.0");
+
+ Category createdCategory = productCategoryRepoService.addCategory(categoryCreate);
+
+ // Step 3: Verify callback was sent
+ verify(categoryCallbackService, timeout(2000)).sendCategoryCreateCallback(any());
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:8080/test-callback/listener/categoryCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+
+ // Step 4: Delete the category (should trigger delete callback)
+ productCategoryRepoService.deleteById(createdCategory.getUuid());
+
+ // Step 5: Verify delete callback was sent
+ verify(categoryCallbackService, timeout(2000)).sendCategoryDeleteCallback(any());
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:8080/test-callback/listener/categoryDeleteEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+ }
+
+ @Test
+ @WithMockUser(username = "osadmin", roles = {"ADMIN"})
+ public void testCallbackFilteringByQuery() throws Exception {
+ // Step 1: Register subscription only for create events
+ EventSubscriptionInput subscriptionInput = new EventSubscriptionInput();
+ subscriptionInput.setCallback("http://localhost:9090/create-only");
+ subscriptionInput.setQuery("category.create");
+
+ mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
+ .with(SecurityMockMvcRequestPostProcessors.csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(JsonUtils.toJson(subscriptionInput)))
+ .andExpect(status().isCreated());
+
+ // Step 2: Create and delete a category
+ CategoryCreate categoryCreate = new CategoryCreate();
+ categoryCreate.setName("Test Filter Category");
+ categoryCreate.setDescription("A category to test query filtering");
+ categoryCreate.setVersion("1.0");
+
+ Category createdCategory = productCategoryRepoService.addCategory(categoryCreate);
+ productCategoryRepoService.deleteById(createdCategory.getUuid());
+
+ // Step 3: Verify only create callback was sent (not delete)
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:9090/create-only/listener/categoryCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+
+ // Note: In a more sophisticated test, we could verify that the delete callback was NOT sent
+ // by using verify with never(), but this requires more complex mock setup
+ }
+
+ @Test
+ @WithMockUser(username = "osadmin", roles = {"ADMIN"})
+ public void testCategoryCallbackWithAllEventsQuery() throws Exception {
+ // Step 1: Register subscription for all events (empty query)
+ EventSubscriptionInput subscriptionInput = new EventSubscriptionInput();
+ subscriptionInput.setCallback("http://localhost:7070/all-events");
+ subscriptionInput.setQuery(""); // Empty query should receive all events
+
+ mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
+ .with(SecurityMockMvcRequestPostProcessors.csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(JsonUtils.toJson(subscriptionInput)))
+ .andExpect(status().isCreated());
+
+ // Step 2: Create a category
+ CategoryCreate categoryCreate = new CategoryCreate();
+ categoryCreate.setName("Test All Events Category");
+ categoryCreate.setDescription("A category to test all events subscription");
+ categoryCreate.setVersion("1.0");
+
+ Category createdCategory = productCategoryRepoService.addCategory(categoryCreate);
+
+ // Step 3: Verify callback was sent even with empty query
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:7070/all-events/listener/categoryCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..df70e6328ef354425aed1bb0f2a313afbaca9d43
--- /dev/null
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackServiceTest.java
@@ -0,0 +1,161 @@
+package org.etsi.osl.services.api.pcm620;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.etsi.osl.tmf.pcm620.model.Category;
+import org.etsi.osl.tmf.pcm620.model.CategoryCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.CategoryDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.EventSubscription;
+import org.etsi.osl.tmf.pcm620.reposervices.CategoryCallbackService;
+import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.web.client.RestTemplate;
+
+@RunWith(SpringRunner.class)
+@ActiveProfiles("testing")
+public class CategoryCallbackServiceTest {
+
+ @Mock
+ private EventSubscriptionRepoService eventSubscriptionRepoService;
+
+ @Mock
+ private RestTemplate restTemplate;
+
+ @InjectMocks
+ private CategoryCallbackService categoryCallbackService;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ public void testSendCategoryCreateCallback() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback");
+ subscription.setQuery("category");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ CategoryCreateEvent event = new CategoryCreateEvent();
+ event.setEventId("test-event-123");
+
+ // Act
+ categoryCallbackService.sendCategoryCreateCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/categoryCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testSendCategoryDeleteCallback() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback");
+ subscription.setQuery("category");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ CategoryDeleteEvent event = new CategoryDeleteEvent();
+ event.setEventId("test-event-456");
+
+ // Act
+ categoryCallbackService.sendCategoryDeleteCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/categoryDeleteEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testCallbackWithTrailingSlash() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback/");
+ subscription.setQuery("category");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ CategoryCreateEvent event = new CategoryCreateEvent();
+ event.setEventId("test-event-789");
+
+ // Act
+ categoryCallbackService.sendCategoryCreateCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/categoryCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testFilterSubscriptionsByQuery() {
+ // Arrange
+ EventSubscription categorySubscription = new EventSubscription();
+ categorySubscription.setCallback("http://localhost:8080/category-callback");
+ categorySubscription.setQuery("category");
+
+ EventSubscription otherSubscription = new EventSubscription();
+ otherSubscription.setCallback("http://localhost:8080/other-callback");
+ otherSubscription.setQuery("catalog");
+
+ List subscriptions = Arrays.asList(categorySubscription, otherSubscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ CategoryCreateEvent event = new CategoryCreateEvent();
+ event.setEventId("test-event-filter");
+
+ // Act
+ categoryCallbackService.sendCategoryCreateCallback(event);
+
+ // Assert - only category subscription should receive callback
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/category-callback/listener/categoryCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CategoryNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryNotificationServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..9de497fc9f1ccd3cb264966547285aa287502467
--- /dev/null
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryNotificationServiceTest.java
@@ -0,0 +1,87 @@
+package org.etsi.osl.services.api.pcm620;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents;
+import org.etsi.osl.tmf.pcm620.model.Category;
+import org.etsi.osl.tmf.pcm620.model.CategoryCreateNotification;
+import org.etsi.osl.tmf.pcm620.model.CategoryDeleteNotification;
+import org.etsi.osl.tmf.pcm620.reposervices.CategoryNotificationService;
+import org.etsi.osl.tmf.pcm620.reposervices.CategoryCallbackService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@ActiveProfiles("testing")
+public class CategoryNotificationServiceTest {
+
+ @Mock
+ private ProductCatalogApiRouteBuilderEvents eventPublisher;
+
+ @Mock
+ private CategoryCallbackService categoryCallbackService;
+
+ @InjectMocks
+ private CategoryNotificationService categoryNotificationService;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ public void testPublishCategoryCreateNotification() {
+ // Arrange
+ Category category = new Category();
+ category.setUuid("test-category-123");
+ category.setName("Test Category");
+ category.setDescription("A test category for notifications");
+
+ // Act
+ categoryNotificationService.publishCategoryCreateNotification(category);
+
+ // Assert
+ verify(eventPublisher, times(1)).publishEvent(any(CategoryCreateNotification.class), eq("test-category-123"));
+ verify(categoryCallbackService, times(1)).sendCategoryCreateCallback(any());
+ }
+
+ @Test
+ public void testPublishCategoryDeleteNotification() {
+ // Arrange
+ Category category = new Category();
+ category.setUuid("test-category-456");
+ category.setName("Test Category to Delete");
+ category.setDescription("A test category for delete notifications");
+
+ // Act
+ categoryNotificationService.publishCategoryDeleteNotification(category);
+
+ // Assert
+ verify(eventPublisher, times(1)).publishEvent(any(CategoryDeleteNotification.class), eq("test-category-456"));
+ verify(categoryCallbackService, times(1)).sendCategoryDeleteCallback(any());
+ }
+
+ @Test
+ public void testCreateNotificationStructure() {
+ // Arrange
+ Category category = new Category();
+ category.setUuid("test-category-789");
+ category.setName("Test Category Structure");
+
+ // Act
+ categoryNotificationService.publishCategoryCreateNotification(category);
+
+ // Assert - verify the notification was published with correct structure
+ verify(eventPublisher).publishEvent(any(CategoryCreateNotification.class), eq("test-category-789"));
+ verify(categoryCallbackService).sendCategoryCreateCallback(any());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java
index 072e514f125eca7dde05aa31585e1e73f2e3bd26..c3d4d0d78e4972ddba7f8698801a63a24170e002 100644
--- a/src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java
@@ -180,7 +180,7 @@ public class HubApiControllerTest {
.andExpect(status().isBadRequest());
}
- @WithMockUser(username = "user", roles = {"USER"})
+ @WithMockUser(username = "user", roles = {"OTHER"})
@Test
public void testRegisterListenerUnauthorized() throws Exception {
File resourceSpecFile = new File("src/test/resources/testPCM620EventSubscriptionInput.json");
@@ -196,7 +196,7 @@ public class HubApiControllerTest {
.andExpect(status().isForbidden());
}
- @WithMockUser(username = "user", roles = {"USER"})
+ @WithMockUser(username = "user", roles = {"OTHER"})
@Test
public void testUnregisterListenerUnauthorized() throws Exception {
// First create a subscription as admin
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..0974606f7a9b09c257049dbdea92a8c51126d045
--- /dev/null
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java
@@ -0,0 +1,254 @@
+package org.etsi.osl.services.api.pcm620;
+
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.etsi.osl.tmf.JsonUtils;
+import org.etsi.osl.tmf.OpenAPISpringBoot;
+import org.etsi.osl.tmf.pcm620.model.ProductOffering;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreate;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingUpdate;
+import org.etsi.osl.tmf.pcm620.model.EventSubscription;
+import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingCallbackService;
+import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingRepoService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.context.WebApplicationContext;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+@RunWith(SpringRunner.class)
+@Transactional
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class)
+@AutoConfigureMockMvc
+@ActiveProfiles("testing")
+@AutoConfigureTestDatabase
+public class ProductOfferingCallbackIntegrationTest {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @Autowired
+ private WebApplicationContext context;
+
+ @Autowired
+ private ProductOfferingRepoService productOfferingRepoService;
+
+ @Autowired
+ private EventSubscriptionRepoService eventSubscriptionRepoService;
+
+ @SpyBean
+ private ProductOfferingCallbackService productOfferingCallbackService;
+
+ @MockBean
+ private RestTemplate restTemplate;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ mvc = MockMvcBuilders
+ .webAppContextSetup(context)
+ .apply(springSecurity())
+ .build();
+
+ // Mock RestTemplate to avoid actual HTTP calls in tests
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK));
+ }
+
+ @Test
+ @WithMockUser(username = "osadmin", roles = {"USER"})
+ public void testCompleteCallbackFlow() throws Exception {
+ // Step 1: Register a callback subscription via Hub API
+ EventSubscriptionInput subscriptionInput = new EventSubscriptionInput();
+ subscriptionInput.setCallback("http://localhost:8080/test-callback");
+ subscriptionInput.setQuery("productoffering.create,productoffering.delete");
+
+ MvcResult subscriptionResult = mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
+ .with(SecurityMockMvcRequestPostProcessors.csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(JsonUtils.toJson(subscriptionInput)))
+ .andExpect(status().isCreated())
+ .andReturn();
+
+ String subscriptionResponseBody = subscriptionResult.getResponse().getContentAsString();
+ EventSubscription createdSubscription = objectMapper.readValue(subscriptionResponseBody, EventSubscription.class);
+
+ // Step 2: Create a product offering (should trigger callback)
+ ProductOfferingCreate productOfferingCreate = new ProductOfferingCreate();
+ productOfferingCreate.setName("Test Callback Product Offering");
+ productOfferingCreate.setDescription("A product offering to test callback notifications");
+ productOfferingCreate.setVersion("1.0");
+
+ ProductOffering createdProductOffering = productOfferingRepoService.addProductOffering(productOfferingCreate);
+
+ // Step 3: Verify callback was sent
+ verify(productOfferingCallbackService, timeout(2000)).sendProductOfferingCreateCallback(any());
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:8080/test-callback/listener/productOfferingCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+
+ // Step 4: Delete the product offering (should trigger delete callback)
+ productOfferingRepoService.deleteByUuid(createdProductOffering.getUuid());
+
+ // Step 5: Verify delete callback was sent
+ verify(productOfferingCallbackService, timeout(2000)).sendProductOfferingDeleteCallback(any());
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:8080/test-callback/listener/productOfferingDeleteEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+ }
+
+ @Test
+ @WithMockUser(username = "osadmin", roles = {"USER"})
+ public void testAttributeValueChangeAndStateChangeCallbacks() throws Exception {
+ // Step 1: Register subscription for attribute and state change events
+ EventSubscriptionInput subscriptionInput = new EventSubscriptionInput();
+ subscriptionInput.setCallback("http://localhost:8080/change-callback");
+ subscriptionInput.setQuery("productoffering.attributevaluechange,productoffering.statechange");
+
+ mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
+ .with(SecurityMockMvcRequestPostProcessors.csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(JsonUtils.toJson(subscriptionInput)))
+ .andExpect(status().isCreated());
+
+ // Step 2: Create a product offering
+ ProductOfferingCreate productOfferingCreate = new ProductOfferingCreate();
+ productOfferingCreate.setName("Test Change Product Offering");
+ productOfferingCreate.setDescription("A product offering to test change notifications");
+ productOfferingCreate.setVersion("1.0");
+ productOfferingCreate.setLifecycleStatus("Active");
+
+ ProductOffering createdProductOffering = productOfferingRepoService.addProductOffering(productOfferingCreate);
+
+ // Step 3: Update the product offering (should trigger attribute value change callback)
+ ProductOfferingUpdate productOfferingUpdate = new ProductOfferingUpdate();
+ productOfferingUpdate.setDescription("Updated description for testing");
+ productOfferingUpdate.setLifecycleStatus("Retired");
+
+ productOfferingRepoService.updateProductOffering(createdProductOffering.getUuid(), productOfferingUpdate);
+
+ // Step 4: Verify both attribute value change and state change callbacks were sent
+ verify(productOfferingCallbackService, timeout(2000)).sendProductOfferingAttributeValueChangeCallback(any());
+ verify(productOfferingCallbackService, timeout(2000)).sendProductOfferingStateChangeCallback(any());
+
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:8080/change-callback/listener/productOfferingAttributeValueChangeEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:8080/change-callback/listener/productOfferingStateChangeEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+ }
+
+ @Test
+ @WithMockUser(username = "osadmin", roles = {"USER"})
+ public void testCallbackFilteringByEventType() throws Exception {
+ // Step 1: Register subscription only for create events
+ EventSubscriptionInput subscriptionInput = new EventSubscriptionInput();
+ subscriptionInput.setCallback("http://localhost:9090/create-only");
+ subscriptionInput.setQuery("productoffering.create");
+
+ mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
+ .with(SecurityMockMvcRequestPostProcessors.csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(JsonUtils.toJson(subscriptionInput)))
+ .andExpect(status().isCreated());
+
+ // Step 2: Create and delete a product offering
+ ProductOfferingCreate productOfferingCreate = new ProductOfferingCreate();
+ productOfferingCreate.setName("Test Filter Product Offering");
+ productOfferingCreate.setDescription("A product offering to test query filtering");
+ productOfferingCreate.setVersion("1.0");
+
+ ProductOffering createdProductOffering = productOfferingRepoService.addProductOffering(productOfferingCreate);
+ productOfferingRepoService.deleteByUuid(createdProductOffering.getUuid());
+
+ // Step 3: Verify only create callback was sent (not delete)
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:9090/create-only/listener/productOfferingCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+
+ // Note: In a more sophisticated test, we could verify that the delete callback was NOT sent
+ // by using verify with never(), but this requires more complex mock setup
+ }
+
+ @Test
+ @WithMockUser(username = "osadmin", roles = {"USER"})
+ public void testProductOfferingCallbackWithAllEventsQuery() throws Exception {
+ // Step 1: Register subscription for all events (empty query)
+ EventSubscriptionInput subscriptionInput = new EventSubscriptionInput();
+ subscriptionInput.setCallback("http://localhost:7070/all-events");
+ subscriptionInput.setQuery(""); // Empty query should receive all events
+
+ mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
+ .with(SecurityMockMvcRequestPostProcessors.csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(JsonUtils.toJson(subscriptionInput)))
+ .andExpect(status().isCreated());
+
+ // Step 2: Create a product offering
+ ProductOfferingCreate productOfferingCreate = new ProductOfferingCreate();
+ productOfferingCreate.setName("Test All Events Product Offering");
+ productOfferingCreate.setDescription("A product offering to test all events subscription");
+ productOfferingCreate.setVersion("1.0");
+
+ ProductOffering createdProductOffering = productOfferingRepoService.addProductOffering(productOfferingCreate);
+
+ // Step 3: Verify callback was sent even with empty query
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:7070/all-events/listener/productOfferingCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..40875e4a444a8d056b7ccf75ca8f1b5ab4f30eac
--- /dev/null
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackServiceTest.java
@@ -0,0 +1,258 @@
+package org.etsi.osl.services.api.pcm620;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.etsi.osl.tmf.pcm620.model.ProductOffering;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.EventSubscription;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingCallbackService;
+import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.web.client.RestTemplate;
+
+@RunWith(SpringRunner.class)
+@ActiveProfiles("testing")
+public class ProductOfferingCallbackServiceTest {
+
+ @Mock
+ private EventSubscriptionRepoService eventSubscriptionRepoService;
+
+ @Mock
+ private RestTemplate restTemplate;
+
+ @InjectMocks
+ private ProductOfferingCallbackService productOfferingCallbackService;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ public void testSendProductOfferingCreateCallback() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback");
+ subscription.setQuery("productoffering");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductOfferingCreateEvent event = new ProductOfferingCreateEvent();
+ event.setEventId("test-event-123");
+
+ // Act
+ productOfferingCallbackService.sendProductOfferingCreateCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/productOfferingCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testSendProductOfferingDeleteCallback() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback");
+ subscription.setQuery("productoffering");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductOfferingDeleteEvent event = new ProductOfferingDeleteEvent();
+ event.setEventId("test-event-456");
+
+ // Act
+ productOfferingCallbackService.sendProductOfferingDeleteCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/productOfferingDeleteEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testSendProductOfferingAttributeValueChangeCallback() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback");
+ subscription.setQuery("productoffering.attributevaluechange");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductOfferingAttributeValueChangeEvent event = new ProductOfferingAttributeValueChangeEvent();
+ event.setEventId("test-event-789");
+
+ // Act
+ productOfferingCallbackService.sendProductOfferingAttributeValueChangeCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/productOfferingAttributeValueChangeEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testSendProductOfferingStateChangeCallback() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback");
+ subscription.setQuery("productoffering.statechange");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductOfferingStateChangeEvent event = new ProductOfferingStateChangeEvent();
+ event.setEventId("test-event-101");
+
+ // Act
+ productOfferingCallbackService.sendProductOfferingStateChangeCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/productOfferingStateChangeEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testCallbackWithTrailingSlash() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback/");
+ subscription.setQuery("productoffering");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductOfferingCreateEvent event = new ProductOfferingCreateEvent();
+ event.setEventId("test-event-trailing-slash");
+
+ // Act
+ productOfferingCallbackService.sendProductOfferingCreateCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/productOfferingCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testFilterSubscriptionsByQuery() {
+ // Arrange
+ EventSubscription productOfferingSubscription = new EventSubscription();
+ productOfferingSubscription.setCallback("http://localhost:8080/productoffering-callback");
+ productOfferingSubscription.setQuery("productoffering");
+
+ EventSubscription otherSubscription = new EventSubscription();
+ otherSubscription.setCallback("http://localhost:8080/other-callback");
+ otherSubscription.setQuery("catalog");
+
+ List subscriptions = Arrays.asList(productOfferingSubscription, otherSubscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductOfferingCreateEvent event = new ProductOfferingCreateEvent();
+ event.setEventId("test-event-filter");
+
+ // Act
+ productOfferingCallbackService.sendProductOfferingCreateCallback(event);
+
+ // Assert - only product offering subscription should receive callback
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/productoffering-callback/listener/productOfferingCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testSpecificEventTypeQueries() {
+ // Arrange
+ EventSubscription createOnlySubscription = new EventSubscription();
+ createOnlySubscription.setCallback("http://localhost:9090/create-only");
+ createOnlySubscription.setQuery("productoffering.create");
+
+ EventSubscription stateChangeOnlySubscription = new EventSubscription();
+ stateChangeOnlySubscription.setCallback("http://localhost:9091/state-change-only");
+ stateChangeOnlySubscription.setQuery("productoffering.statechange");
+
+ List subscriptions = Arrays.asList(createOnlySubscription, stateChangeOnlySubscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductOfferingCreateEvent createEvent = new ProductOfferingCreateEvent();
+ createEvent.setEventId("test-create-event");
+
+ ProductOfferingStateChangeEvent stateChangeEvent = new ProductOfferingStateChangeEvent();
+ stateChangeEvent.setEventId("test-state-change-event");
+
+ // Act
+ productOfferingCallbackService.sendProductOfferingCreateCallback(createEvent);
+ productOfferingCallbackService.sendProductOfferingStateChangeCallback(stateChangeEvent);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:9090/create-only/listener/productOfferingCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:9091/state-change-only/listener/productOfferingStateChangeEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingNotificationServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..037f60ce66106f16b83dfb61c806ff8e67031b16
--- /dev/null
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingNotificationServiceTest.java
@@ -0,0 +1,121 @@
+package org.etsi.osl.services.api.pcm620;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents;
+import org.etsi.osl.tmf.pcm620.model.ProductOffering;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeNotification;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingNotificationService;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingCallbackService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@ActiveProfiles("testing")
+public class ProductOfferingNotificationServiceTest {
+
+ @Mock
+ private ProductCatalogApiRouteBuilderEvents eventPublisher;
+
+ @Mock
+ private ProductOfferingCallbackService productOfferingCallbackService;
+
+ @InjectMocks
+ private ProductOfferingNotificationService productOfferingNotificationService;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ public void testPublishProductOfferingCreateNotification() {
+ // Arrange
+ ProductOffering productOffering = new ProductOffering();
+ productOffering.setUuid("test-productoffering-123");
+ productOffering.setName("Test Product Offering");
+ productOffering.setDescription("A test product offering for notifications");
+
+ // Act
+ productOfferingNotificationService.publishProductOfferingCreateNotification(productOffering);
+
+ // Assert
+ verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingCreateNotification.class), eq("test-productoffering-123"));
+ verify(productOfferingCallbackService, times(1)).sendProductOfferingCreateCallback(any());
+ }
+
+ @Test
+ public void testPublishProductOfferingDeleteNotification() {
+ // Arrange
+ ProductOffering productOffering = new ProductOffering();
+ productOffering.setUuid("test-productoffering-456");
+ productOffering.setName("Test Product Offering to Delete");
+ productOffering.setDescription("A test product offering for delete notifications");
+
+ // Act
+ productOfferingNotificationService.publishProductOfferingDeleteNotification(productOffering);
+
+ // Assert
+ verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingDeleteNotification.class), eq("test-productoffering-456"));
+ verify(productOfferingCallbackService, times(1)).sendProductOfferingDeleteCallback(any());
+ }
+
+ @Test
+ public void testPublishProductOfferingAttributeValueChangeNotification() {
+ // Arrange
+ ProductOffering productOffering = new ProductOffering();
+ productOffering.setUuid("test-productoffering-789");
+ productOffering.setName("Test Product Offering Attribute Change");
+ productOffering.setDescription("A test product offering for attribute change notifications");
+
+ // Act
+ productOfferingNotificationService.publishProductOfferingAttributeValueChangeNotification(productOffering);
+
+ // Assert
+ verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingAttributeValueChangeNotification.class), eq("test-productoffering-789"));
+ verify(productOfferingCallbackService, times(1)).sendProductOfferingAttributeValueChangeCallback(any());
+ }
+
+ @Test
+ public void testPublishProductOfferingStateChangeNotification() {
+ // Arrange
+ ProductOffering productOffering = new ProductOffering();
+ productOffering.setUuid("test-productoffering-101");
+ productOffering.setName("Test Product Offering State Change");
+ productOffering.setDescription("A test product offering for state change notifications");
+
+ // Act
+ productOfferingNotificationService.publishProductOfferingStateChangeNotification(productOffering);
+
+ // Assert
+ verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingStateChangeNotification.class), eq("test-productoffering-101"));
+ verify(productOfferingCallbackService, times(1)).sendProductOfferingStateChangeCallback(any());
+ }
+
+ @Test
+ public void testCreateNotificationStructure() {
+ // Arrange
+ ProductOffering productOffering = new ProductOffering();
+ productOffering.setUuid("test-productoffering-structure");
+ productOffering.setName("Test Product Offering Structure");
+
+ // Act
+ productOfferingNotificationService.publishProductOfferingCreateNotification(productOffering);
+
+ // Assert - verify the notification was published with correct structure
+ verify(eventPublisher).publishEvent(any(ProductOfferingCreateNotification.class), eq("test-productoffering-structure"));
+ verify(productOfferingCallbackService).sendProductOfferingCreateCallback(any());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceCallbackServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceCallbackServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8e8b04a2b86580d77f87782f190d73fb2c7e3a4b
--- /dev/null
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceCallbackServiceTest.java
@@ -0,0 +1,258 @@
+package org.etsi.osl.services.api.pcm620;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPrice;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeEvent;
+import org.etsi.osl.tmf.pcm620.model.EventSubscription;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingPriceCallbackService;
+import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.web.client.RestTemplate;
+
+@RunWith(SpringRunner.class)
+@ActiveProfiles("testing")
+public class ProductOfferingPriceCallbackServiceTest {
+
+ @Mock
+ private EventSubscriptionRepoService eventSubscriptionRepoService;
+
+ @Mock
+ private RestTemplate restTemplate;
+
+ @InjectMocks
+ private ProductOfferingPriceCallbackService productOfferingPriceCallbackService;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ public void testSendProductOfferingPriceCreateCallback() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback");
+ subscription.setQuery("productofferingprice");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductOfferingPriceCreateEvent event = new ProductOfferingPriceCreateEvent();
+ event.setEventId("test-event-123");
+
+ // Act
+ productOfferingPriceCallbackService.sendProductOfferingPriceCreateCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/productOfferingPriceCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testSendProductOfferingPriceDeleteCallback() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback");
+ subscription.setQuery("productofferingprice");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductOfferingPriceDeleteEvent event = new ProductOfferingPriceDeleteEvent();
+ event.setEventId("test-event-456");
+
+ // Act
+ productOfferingPriceCallbackService.sendProductOfferingPriceDeleteCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/productOfferingPriceDeleteEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testSendProductOfferingPriceAttributeValueChangeCallback() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback");
+ subscription.setQuery("productofferingprice.attributevaluechange");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductOfferingPriceAttributeValueChangeEvent event = new ProductOfferingPriceAttributeValueChangeEvent();
+ event.setEventId("test-event-789");
+
+ // Act
+ productOfferingPriceCallbackService.sendProductOfferingPriceAttributeValueChangeCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/productOfferingPriceAttributeValueChangeEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testSendProductOfferingPriceStateChangeCallback() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback");
+ subscription.setQuery("productofferingprice.statechange");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductOfferingPriceStateChangeEvent event = new ProductOfferingPriceStateChangeEvent();
+ event.setEventId("test-event-101");
+
+ // Act
+ productOfferingPriceCallbackService.sendProductOfferingPriceStateChangeCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/productOfferingPriceStateChangeEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testCallbackWithTrailingSlash() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback/");
+ subscription.setQuery("productofferingprice");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductOfferingPriceCreateEvent event = new ProductOfferingPriceCreateEvent();
+ event.setEventId("test-event-trailing-slash");
+
+ // Act
+ productOfferingPriceCallbackService.sendProductOfferingPriceCreateCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/productOfferingPriceCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testFilterSubscriptionsByQuery() {
+ // Arrange
+ EventSubscription productOfferingPriceSubscription = new EventSubscription();
+ productOfferingPriceSubscription.setCallback("http://localhost:8080/productofferingprice-callback");
+ productOfferingPriceSubscription.setQuery("productofferingprice");
+
+ EventSubscription otherSubscription = new EventSubscription();
+ otherSubscription.setCallback("http://localhost:8080/other-callback");
+ otherSubscription.setQuery("catalog");
+
+ List subscriptions = Arrays.asList(productOfferingPriceSubscription, otherSubscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductOfferingPriceCreateEvent event = new ProductOfferingPriceCreateEvent();
+ event.setEventId("test-event-filter");
+
+ // Act
+ productOfferingPriceCallbackService.sendProductOfferingPriceCreateCallback(event);
+
+ // Assert - only product offering price subscription should receive callback
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/productofferingprice-callback/listener/productOfferingPriceCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testSpecificEventTypeQueries() {
+ // Arrange
+ EventSubscription createOnlySubscription = new EventSubscription();
+ createOnlySubscription.setCallback("http://localhost:9090/create-only");
+ createOnlySubscription.setQuery("productofferingprice.create");
+
+ EventSubscription stateChangeOnlySubscription = new EventSubscription();
+ stateChangeOnlySubscription.setCallback("http://localhost:9091/state-change-only");
+ stateChangeOnlySubscription.setQuery("productofferingprice.statechange");
+
+ List subscriptions = Arrays.asList(createOnlySubscription, stateChangeOnlySubscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductOfferingPriceCreateEvent createEvent = new ProductOfferingPriceCreateEvent();
+ createEvent.setEventId("test-create-event");
+
+ ProductOfferingPriceStateChangeEvent stateChangeEvent = new ProductOfferingPriceStateChangeEvent();
+ stateChangeEvent.setEventId("test-state-change-event");
+
+ // Act
+ productOfferingPriceCallbackService.sendProductOfferingPriceCreateCallback(createEvent);
+ productOfferingPriceCallbackService.sendProductOfferingPriceStateChangeCallback(stateChangeEvent);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:9090/create-only/listener/productOfferingPriceCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:9091/state-change-only/listener/productOfferingPriceStateChangeEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceNotificationServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e31a0b8cb291388935747e3e40ddf743e2d94949
--- /dev/null
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceNotificationServiceTest.java
@@ -0,0 +1,121 @@
+package org.etsi.osl.services.api.pcm620;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPrice;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeNotification;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingPriceNotificationService;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingPriceCallbackService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@ActiveProfiles("testing")
+public class ProductOfferingPriceNotificationServiceTest {
+
+ @Mock
+ private ProductCatalogApiRouteBuilderEvents eventPublisher;
+
+ @Mock
+ private ProductOfferingPriceCallbackService productOfferingPriceCallbackService;
+
+ @InjectMocks
+ private ProductOfferingPriceNotificationService productOfferingPriceNotificationService;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ public void testPublishProductOfferingPriceCreateNotification() {
+ // Arrange
+ ProductOfferingPrice productOfferingPrice = new ProductOfferingPrice();
+ productOfferingPrice.setUuid("test-productofferingprice-123");
+ productOfferingPrice.setName("Test Product Offering Price");
+ productOfferingPrice.setDescription("A test product offering price for notifications");
+
+ // Act
+ productOfferingPriceNotificationService.publishProductOfferingPriceCreateNotification(productOfferingPrice);
+
+ // Assert
+ verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingPriceCreateNotification.class), eq("test-productofferingprice-123"));
+ verify(productOfferingPriceCallbackService, times(1)).sendProductOfferingPriceCreateCallback(any());
+ }
+
+ @Test
+ public void testPublishProductOfferingPriceDeleteNotification() {
+ // Arrange
+ ProductOfferingPrice productOfferingPrice = new ProductOfferingPrice();
+ productOfferingPrice.setUuid("test-productofferingprice-456");
+ productOfferingPrice.setName("Test Product Offering Price to Delete");
+ productOfferingPrice.setDescription("A test product offering price for delete notifications");
+
+ // Act
+ productOfferingPriceNotificationService.publishProductOfferingPriceDeleteNotification(productOfferingPrice);
+
+ // Assert
+ verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingPriceDeleteNotification.class), eq("test-productofferingprice-456"));
+ verify(productOfferingPriceCallbackService, times(1)).sendProductOfferingPriceDeleteCallback(any());
+ }
+
+ @Test
+ public void testPublishProductOfferingPriceAttributeValueChangeNotification() {
+ // Arrange
+ ProductOfferingPrice productOfferingPrice = new ProductOfferingPrice();
+ productOfferingPrice.setUuid("test-productofferingprice-789");
+ productOfferingPrice.setName("Test Product Offering Price Attribute Change");
+ productOfferingPrice.setDescription("A test product offering price for attribute change notifications");
+
+ // Act
+ productOfferingPriceNotificationService.publishProductOfferingPriceAttributeValueChangeNotification(productOfferingPrice);
+
+ // Assert
+ verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingPriceAttributeValueChangeNotification.class), eq("test-productofferingprice-789"));
+ verify(productOfferingPriceCallbackService, times(1)).sendProductOfferingPriceAttributeValueChangeCallback(any());
+ }
+
+ @Test
+ public void testPublishProductOfferingPriceStateChangeNotification() {
+ // Arrange
+ ProductOfferingPrice productOfferingPrice = new ProductOfferingPrice();
+ productOfferingPrice.setUuid("test-productofferingprice-101");
+ productOfferingPrice.setName("Test Product Offering Price State Change");
+ productOfferingPrice.setDescription("A test product offering price for state change notifications");
+
+ // Act
+ productOfferingPriceNotificationService.publishProductOfferingPriceStateChangeNotification(productOfferingPrice);
+
+ // Assert
+ verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingPriceStateChangeNotification.class), eq("test-productofferingprice-101"));
+ verify(productOfferingPriceCallbackService, times(1)).sendProductOfferingPriceStateChangeCallback(any());
+ }
+
+ @Test
+ public void testCreateNotificationStructure() {
+ // Arrange
+ ProductOfferingPrice productOfferingPrice = new ProductOfferingPrice();
+ productOfferingPrice.setUuid("test-productofferingprice-structure");
+ productOfferingPrice.setName("Test Product Offering Price Structure");
+
+ // Act
+ productOfferingPriceNotificationService.publishProductOfferingPriceCreateNotification(productOfferingPrice);
+
+ // Assert - verify the notification was published with correct structure
+ verify(eventPublisher).publishEvent(any(ProductOfferingPriceCreateNotification.class), eq("test-productofferingprice-structure"));
+ verify(productOfferingPriceCallbackService).sendProductOfferingPriceCreateCallback(any());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..f85b8671985b7e4dd42da287795d6ca5ac938137
--- /dev/null
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackIntegrationTest.java
@@ -0,0 +1,205 @@
+package org.etsi.osl.services.api.pcm620;
+
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.etsi.osl.tmf.JsonUtils;
+import org.etsi.osl.tmf.OpenAPISpringBoot;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecification;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreate;
+import org.etsi.osl.tmf.pcm620.model.EventSubscription;
+import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductSpecificationCallbackService;
+import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductSpecificationRepoService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.context.WebApplicationContext;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+@RunWith(SpringRunner.class)
+@Transactional
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class)
+@AutoConfigureMockMvc
+@ActiveProfiles("testing")
+@AutoConfigureTestDatabase
+public class ProductSpecificationCallbackIntegrationTest {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @Autowired
+ private WebApplicationContext context;
+
+ @Autowired
+ private ProductSpecificationRepoService productSpecificationRepoService;
+
+ @Autowired
+ private EventSubscriptionRepoService eventSubscriptionRepoService;
+
+ @SpyBean
+ private ProductSpecificationCallbackService productSpecificationCallbackService;
+
+ @MockBean
+ private RestTemplate restTemplate;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ mvc = MockMvcBuilders
+ .webAppContextSetup(context)
+ .apply(springSecurity())
+ .build();
+
+ // Mock RestTemplate to avoid actual HTTP calls in tests
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK));
+ }
+
+ @Test
+ @WithMockUser(username = "osadmin", roles = {"ADMIN"})
+ public void testCompleteCallbackFlow() throws Exception {
+ // Step 1: Register a callback subscription via Hub API
+ EventSubscriptionInput subscriptionInput = new EventSubscriptionInput();
+ subscriptionInput.setCallback("http://localhost:8080/test-callback");
+ subscriptionInput.setQuery("productspecification.create,productspecification.delete");
+
+ MvcResult subscriptionResult = mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
+ .with(SecurityMockMvcRequestPostProcessors.csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(JsonUtils.toJson(subscriptionInput)))
+ .andExpect(status().isCreated())
+ .andReturn();
+
+ String subscriptionResponseBody = subscriptionResult.getResponse().getContentAsString();
+ EventSubscription createdSubscription = objectMapper.readValue(subscriptionResponseBody, EventSubscription.class);
+
+ // Step 2: Create a product specification (should trigger callback)
+ ProductSpecificationCreate productSpecificationCreate = new ProductSpecificationCreate();
+ productSpecificationCreate.setName("Test Callback Product Specification");
+ productSpecificationCreate.setDescription("A product specification to test callback notifications");
+ productSpecificationCreate.setVersion("1.0");
+
+ ProductSpecification createdProductSpecification = productSpecificationRepoService.addProductSpecification(productSpecificationCreate);
+
+ // Step 3: Verify callback was sent
+ verify(productSpecificationCallbackService, timeout(2000)).sendProductSpecificationCreateCallback(any());
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:8080/test-callback/listener/productSpecificationCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+
+ // Step 4: Delete the product specification (should trigger delete callback)
+ productSpecificationRepoService.deleteByUuid(createdProductSpecification.getUuid());
+
+ // Step 5: Verify delete callback was sent
+ verify(productSpecificationCallbackService, timeout(2000)).sendProductSpecificationDeleteCallback(any());
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:8080/test-callback/listener/productSpecificationDeleteEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+ }
+
+ @Test
+ @WithMockUser(username = "osadmin", roles = {"ADMIN"})
+ public void testCallbackFilteringByQuery() throws Exception {
+ // Step 1: Register subscription only for create events
+ EventSubscriptionInput subscriptionInput = new EventSubscriptionInput();
+ subscriptionInput.setCallback("http://localhost:9090/create-only");
+ subscriptionInput.setQuery("productspecification.create");
+
+ mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
+ .with(SecurityMockMvcRequestPostProcessors.csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(JsonUtils.toJson(subscriptionInput)))
+ .andExpect(status().isCreated());
+
+ // Step 2: Create and delete a product specification
+ ProductSpecificationCreate productSpecificationCreate = new ProductSpecificationCreate();
+ productSpecificationCreate.setName("Test Filter Product Specification");
+ productSpecificationCreate.setDescription("A product specification to test query filtering");
+ productSpecificationCreate.setVersion("1.0");
+
+ ProductSpecification createdProductSpecification = productSpecificationRepoService.addProductSpecification(productSpecificationCreate);
+ productSpecificationRepoService.deleteByUuid(createdProductSpecification.getUuid());
+
+ // Step 3: Verify only create callback was sent (not delete)
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:9090/create-only/listener/productSpecificationCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+
+ // Note: In a more sophisticated test, we could verify that the delete callback was NOT sent
+ // by using verify with never(), but this requires more complex mock setup
+ }
+
+ @Test
+ @WithMockUser(username = "osadmin", roles = {"ADMIN"})
+ public void testProductSpecificationCallbackWithAllEventsQuery() throws Exception {
+ // Step 1: Register subscription for all events (empty query)
+ EventSubscriptionInput subscriptionInput = new EventSubscriptionInput();
+ subscriptionInput.setCallback("http://localhost:7070/all-events");
+ subscriptionInput.setQuery(""); // Empty query should receive all events
+
+ mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub")
+ .with(SecurityMockMvcRequestPostProcessors.csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(JsonUtils.toJson(subscriptionInput)))
+ .andExpect(status().isCreated());
+
+ // Step 2: Create a product specification
+ ProductSpecificationCreate productSpecificationCreate = new ProductSpecificationCreate();
+ productSpecificationCreate.setName("Test All Events Product Specification");
+ productSpecificationCreate.setDescription("A product specification to test all events subscription");
+ productSpecificationCreate.setVersion("1.0");
+
+ ProductSpecification createdProductSpecification = productSpecificationRepoService.addProductSpecification(productSpecificationCreate);
+
+ // Step 3: Verify callback was sent even with empty query
+ verify(restTemplate, timeout(2000)).exchange(
+ eq("http://localhost:7070/all-events/listener/productSpecificationCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1d11a49d93221ced818804c5a3dfc9055fe5f916
--- /dev/null
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackServiceTest.java
@@ -0,0 +1,188 @@
+package org.etsi.osl.services.api.pcm620;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.etsi.osl.tmf.pcm620.model.ProductSpecification;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateEvent;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteEvent;
+import org.etsi.osl.tmf.pcm620.model.EventSubscription;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductSpecificationCallbackService;
+import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.web.client.RestTemplate;
+
+@RunWith(SpringRunner.class)
+@ActiveProfiles("testing")
+public class ProductSpecificationCallbackServiceTest {
+
+ @Mock
+ private EventSubscriptionRepoService eventSubscriptionRepoService;
+
+ @Mock
+ private RestTemplate restTemplate;
+
+ @InjectMocks
+ private ProductSpecificationCallbackService productSpecificationCallbackService;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ public void testSendProductSpecificationCreateCallback() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback");
+ subscription.setQuery("productspecification");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductSpecificationCreateEvent event = new ProductSpecificationCreateEvent();
+ event.setEventId("test-event-123");
+
+ // Act
+ productSpecificationCallbackService.sendProductSpecificationCreateCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/productSpecificationCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testSendProductSpecificationDeleteCallback() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback");
+ subscription.setQuery("productspecification");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductSpecificationDeleteEvent event = new ProductSpecificationDeleteEvent();
+ event.setEventId("test-event-456");
+
+ // Act
+ productSpecificationCallbackService.sendProductSpecificationDeleteCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/productSpecificationDeleteEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testCallbackWithTrailingSlash() {
+ // Arrange
+ EventSubscription subscription = new EventSubscription();
+ subscription.setCallback("http://localhost:8080/callback/");
+ subscription.setQuery("productspecification");
+
+ List subscriptions = Arrays.asList(subscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductSpecificationCreateEvent event = new ProductSpecificationCreateEvent();
+ event.setEventId("test-event-789");
+
+ // Act
+ productSpecificationCallbackService.sendProductSpecificationCreateCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/callback/listener/productSpecificationCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testFilterSubscriptionsByQuery() {
+ // Arrange
+ EventSubscription productSpecSubscription = new EventSubscription();
+ productSpecSubscription.setCallback("http://localhost:8080/productspec-callback");
+ productSpecSubscription.setQuery("productspecification");
+
+ EventSubscription otherSubscription = new EventSubscription();
+ otherSubscription.setCallback("http://localhost:8080/other-callback");
+ otherSubscription.setQuery("catalog");
+
+ List subscriptions = Arrays.asList(productSpecSubscription, otherSubscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductSpecificationCreateEvent event = new ProductSpecificationCreateEvent();
+ event.setEventId("test-event-filter");
+
+ // Act
+ productSpecificationCallbackService.sendProductSpecificationCreateCallback(event);
+
+ // Assert - only product specification subscription should receive callback
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:8080/productspec-callback/listener/productSpecificationCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+
+ @Test
+ public void testProductSpecificationSpecificQueries() {
+ // Arrange
+ EventSubscription createOnlySubscription = new EventSubscription();
+ createOnlySubscription.setCallback("http://localhost:9090/create-only");
+ createOnlySubscription.setQuery("productspecification.create");
+
+ List subscriptions = Arrays.asList(createOnlySubscription);
+ when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions);
+ when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)))
+ .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK));
+
+ ProductSpecificationCreateEvent event = new ProductSpecificationCreateEvent();
+ event.setEventId("test-event-specific-query");
+
+ // Act
+ productSpecificationCallbackService.sendProductSpecificationCreateCallback(event);
+
+ // Assert
+ verify(restTemplate, times(1)).exchange(
+ eq("http://localhost:9090/create-only/listener/productSpecificationCreateEvent"),
+ eq(HttpMethod.POST),
+ any(HttpEntity.class),
+ eq(String.class)
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationNotificationServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b251a05346710ecf0102bf0fa8b32493e3283979
--- /dev/null
+++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationNotificationServiceTest.java
@@ -0,0 +1,87 @@
+package org.etsi.osl.services.api.pcm620;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecification;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateNotification;
+import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteNotification;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductSpecificationNotificationService;
+import org.etsi.osl.tmf.pcm620.reposervices.ProductSpecificationCallbackService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@ActiveProfiles("testing")
+public class ProductSpecificationNotificationServiceTest {
+
+ @Mock
+ private ProductCatalogApiRouteBuilderEvents eventPublisher;
+
+ @Mock
+ private ProductSpecificationCallbackService productSpecificationCallbackService;
+
+ @InjectMocks
+ private ProductSpecificationNotificationService productSpecificationNotificationService;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ public void testPublishProductSpecificationCreateNotification() {
+ // Arrange
+ ProductSpecification productSpecification = new ProductSpecification();
+ productSpecification.setUuid("test-productspec-123");
+ productSpecification.setName("Test Product Specification");
+ productSpecification.setDescription("A test product specification for notifications");
+
+ // Act
+ productSpecificationNotificationService.publishProductSpecificationCreateNotification(productSpecification);
+
+ // Assert
+ verify(eventPublisher, times(1)).publishEvent(any(ProductSpecificationCreateNotification.class), eq("test-productspec-123"));
+ verify(productSpecificationCallbackService, times(1)).sendProductSpecificationCreateCallback(any());
+ }
+
+ @Test
+ public void testPublishProductSpecificationDeleteNotification() {
+ // Arrange
+ ProductSpecification productSpecification = new ProductSpecification();
+ productSpecification.setUuid("test-productspec-456");
+ productSpecification.setName("Test Product Specification to Delete");
+ productSpecification.setDescription("A test product specification for delete notifications");
+
+ // Act
+ productSpecificationNotificationService.publishProductSpecificationDeleteNotification(productSpecification);
+
+ // Assert
+ verify(eventPublisher, times(1)).publishEvent(any(ProductSpecificationDeleteNotification.class), eq("test-productspec-456"));
+ verify(productSpecificationCallbackService, times(1)).sendProductSpecificationDeleteCallback(any());
+ }
+
+ @Test
+ public void testCreateNotificationStructure() {
+ // Arrange
+ ProductSpecification productSpecification = new ProductSpecification();
+ productSpecification.setUuid("test-productspec-789");
+ productSpecification.setName("Test Product Specification Structure");
+
+ // Act
+ productSpecificationNotificationService.publishProductSpecificationCreateNotification(productSpecification);
+
+ // Assert - verify the notification was published with correct structure
+ verify(eventPublisher).publishEvent(any(ProductSpecificationCreateNotification.class), eq("test-productspec-789"));
+ verify(productSpecificationCallbackService).sendProductSpecificationCreateCallback(any());
+ }
+}
\ No newline at end of file