2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2024 Nordix Foundation
4 * ================================================================================
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
17 * SPDX-License-Identifier: Apache-2.0
18 * ============LICENSE_END=========================================================
21 package org.onap.cps.ncmp.api.impl.events.cmsubscription.service;
23 import static org.onap.cps.spi.FetchDescendantsOption.DIRECT_CHILDREN_ONLY;
24 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS;
26 import java.io.Serializable;
27 import java.time.OffsetDateTime;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.List;
32 import lombok.RequiredArgsConstructor;
33 import lombok.extern.slf4j.Slf4j;
34 import org.onap.cps.api.CpsDataService;
35 import org.onap.cps.api.CpsQueryService;
36 import org.onap.cps.ncmp.api.impl.operations.DatastoreType;
37 import org.onap.cps.spi.model.DataNode;
38 import org.onap.cps.utils.ContentType;
39 import org.onap.cps.utils.JsonObjectMapper;
40 import org.springframework.stereotype.Service;
44 @RequiredArgsConstructor
45 public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotificationSubscriptionPersistenceService {
47 private static final String SUBSCRIPTION_ANCHOR_NAME = "cm-data-subscriptions";
48 private static final String CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE = """
49 /datastores/datastore[@name='%s']/cm-handles/cm-handle[@id='%s']
51 private static final String CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE =
52 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE + "/filters";
54 private static final String CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH =
55 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE + "/filter[@xpath='%s']";
58 private static final String CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_ID = """
59 //filter/subscriptionIds[text()='%s']
62 private final JsonObjectMapper jsonObjectMapper;
63 private final CpsQueryService cpsQueryService;
64 private final CpsDataService cpsDataService;
67 public boolean isOngoingCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
69 return !getOngoingCmNotificationSubscriptionIds(datastoreType, cmHandleId, xpath).isEmpty();
73 public boolean isUniqueSubscriptionId(final String subscriptionId) {
74 return cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME,
75 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_ID.formatted(subscriptionId),
76 OMIT_DESCENDANTS).isEmpty();
80 public Collection<String> getOngoingCmNotificationSubscriptionIds(final DatastoreType datastoreType,
81 final String cmHandleId, final String xpath) {
83 final String isOngoingCmSubscriptionCpsPathQuery =
84 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted(
85 datastoreType.getDatastoreName(), cmHandleId, escapeQuotesByDoublingThem(xpath));
86 final Collection<DataNode> existingNodes =
87 cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME,
88 isOngoingCmSubscriptionCpsPathQuery, OMIT_DESCENDANTS);
89 if (existingNodes.isEmpty()) {
90 return Collections.emptyList();
92 return (List<String>) existingNodes.iterator().next().getLeaves().get("subscriptionIds");
96 public void addCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
97 final String xpath, final String subscriptionId) {
98 final Collection<String> subscriptionIds = getOngoingCmNotificationSubscriptionIds(datastoreType,
100 if (subscriptionIds.isEmpty()) {
101 addFirstSubscriptionForDatastoreCmHandleAndXpath(datastoreType, cmHandleId, xpath, subscriptionId);
102 } else if (!subscriptionIds.contains(subscriptionId)) {
103 subscriptionIds.add(subscriptionId);
104 saveSubscriptionDetails(datastoreType, cmHandleId, xpath, subscriptionIds);
109 public void removeCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
110 final String xpath, final String subscriptionId) {
111 final Collection<String> subscriptionIds = getOngoingCmNotificationSubscriptionIds(datastoreType,
113 if (subscriptionIds.remove(subscriptionId)) {
114 saveSubscriptionDetails(datastoreType, cmHandleId, xpath, subscriptionIds);
115 log.info("There are subscribers left for the following cps path {} :",
116 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted(
117 datastoreType.getDatastoreName(), cmHandleId, escapeQuotesByDoublingThem(xpath)));
118 if (subscriptionIds.isEmpty()) {
119 log.info("No subscribers left for the following cps path {} :",
120 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted(
121 datastoreType.getDatastoreName(), cmHandleId, escapeQuotesByDoublingThem(xpath)));
122 deleteListOfSubscriptionsFor(datastoreType, cmHandleId, xpath);
127 private void deleteListOfSubscriptionsFor(final DatastoreType datastoreType, final String cmHandleId,
128 final String xpath) {
129 cpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME,
130 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted(
131 datastoreType.getDatastoreName(), cmHandleId, escapeQuotesByDoublingThem(xpath)),
132 OffsetDateTime.now());
133 final Collection<DataNode> existingFiltersForCmHandle =
134 cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME,
135 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE.formatted(
136 datastoreType.getDatastoreName(), cmHandleId),
137 DIRECT_CHILDREN_ONLY).iterator().next().getChildDataNodes();
138 if (existingFiltersForCmHandle.isEmpty()) {
139 removeCmHandleFromDatastore(datastoreType.getDatastoreName(), cmHandleId);
143 private void removeCmHandleFromDatastore(final String datastoreName, final String cmHandleId) {
144 cpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME,
145 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE.formatted(
146 datastoreName, cmHandleId), OffsetDateTime.now());
149 private boolean isFirstSubscriptionForCmHandle(final DatastoreType datastoreType, final String cmHandleId) {
150 return cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME,
151 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE.formatted(
152 datastoreType.getDatastoreName(), cmHandleId),
153 OMIT_DESCENDANTS).isEmpty();
156 private void addFirstSubscriptionForDatastoreCmHandleAndXpath(final DatastoreType datastoreType,
157 final String cmHandleId,
159 final String subscriptionId) {
160 final Collection<String> newSubscriptionList = Collections.singletonList(subscriptionId);
161 final String subscriptionDetailsAsJson = getSubscriptionDetailsAsJson(xpath, newSubscriptionList);
162 if (isFirstSubscriptionForCmHandle(datastoreType, cmHandleId)) {
163 final String parentXpath = "/datastores/datastore[@name='%s']/cm-handles"
164 .formatted(datastoreType.getDatastoreName());
165 final String subscriptionAsJson = String.format("{\"cm-handle\":[{\"id\":\"%s\",\"filters\":%s}]}",
166 cmHandleId, subscriptionDetailsAsJson);
167 cpsDataService.saveData(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, parentXpath, subscriptionAsJson,
168 OffsetDateTime.now(), ContentType.JSON);
170 cpsDataService.saveListElements(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME,
171 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE.formatted(
172 datastoreType.getDatastoreName(), cmHandleId),
173 subscriptionDetailsAsJson, OffsetDateTime.now());
177 private void saveSubscriptionDetails(final DatastoreType datastoreType, final String cmHandleId,
179 final Collection<String> subscriptionIds) {
180 final String subscriptionDetailsAsJson = getSubscriptionDetailsAsJson(xpath, subscriptionIds);
181 cpsDataService.updateNodeLeaves(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME,
182 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE.formatted(
183 datastoreType.getDatastoreName(), cmHandleId), subscriptionDetailsAsJson, OffsetDateTime.now());
186 private String getSubscriptionDetailsAsJson(final String xpath, final Collection<String> subscriptionIds) {
187 final Map<String, Serializable> subscriptionDetailsAsMap =
188 Map.of("xpath", xpath, "subscriptionIds", (Serializable) subscriptionIds);
189 return "{\"filter\":[" + jsonObjectMapper.asJsonString(subscriptionDetailsAsMap) + "]}";
192 private static String escapeQuotesByDoublingThem(final String inputXpath) {
193 return inputXpath.replace("'", "''");