c24507a1a7fffb6d79b2853d387117e2286916ea
[cps.git] /
1 /*
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
8  *
9  *        http://www.apache.org/licenses/LICENSE-2.0
10  *
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.
16  *
17  *  SPDX-License-Identifier: Apache-2.0
18  *  ============LICENSE_END=========================================================
19  */
20
21 package org.onap.cps.ncmp.impl.cmnotificationsubscription.utils;
22
23 import static org.onap.cps.spi.FetchDescendantsOption.DIRECT_CHILDREN_ONLY;
24 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS;
25
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;
31 import java.util.Map;
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.data.models.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;
41
42 @Slf4j
43 @Service
44 @RequiredArgsConstructor
45 public class CmSubscriptionPersistenceService {
46
47     private static final String NCMP_DATASPACE_NAME = "NCMP-Admin";
48     private static final String CM_SUBSCRIPTIONS_ANCHOR_NAME = "cm-data-subscriptions";
49
50     private static final String SUBSCRIPTION_ANCHOR_NAME = "cm-data-subscriptions";
51     private static final String CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE = """
52             /datastores/datastore[@name='%s']/cm-handles/cm-handle[@id='%s']
53             """.trim();
54     private static final String CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE =
55             CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE + "/filters";
56
57     private static final String CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH =
58             CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE + "/filter[@xpath='%s']";
59
60
61     private static final String CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_ID = """
62             //filter/subscriptionIds[text()='%s']
63             """.trim();
64
65     private final JsonObjectMapper jsonObjectMapper;
66     private final CpsQueryService cpsQueryService;
67     private final CpsDataService cpsDataService;
68
69     /**
70      * Check if we have an ongoing cm subscription based on the parameters.
71      *
72      * @param datastoreType the susbcription target datastore type
73      * @param cmHandleId    the id of the cm handle for the susbcription
74      * @param xpath         the target xpath
75      * @return true for ongoing cmsubscription , otherwise false
76      */
77     public boolean isOngoingCmSubscription(final DatastoreType datastoreType, final String cmHandleId,
78             final String xpath) {
79         return !getOngoingCmSubscriptionIds(datastoreType, cmHandleId, xpath).isEmpty();
80     }
81
82     /**
83      * Check if the subscription ID is unique against ongoing subscriptions.
84      *
85      * @param subscriptionId subscription ID
86      * @return true if subscriptionId is not used in active subscriptions, otherwise false
87      */
88     public boolean isUniqueSubscriptionId(final String subscriptionId) {
89         return cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME,
90                 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_ID.formatted(subscriptionId), OMIT_DESCENDANTS).isEmpty();
91     }
92
93     /**
94      * Get all ongoing cm notification subscription based on the parameters.
95      *
96      * @param datastoreType the susbcription target datastore type
97      * @param cmHandleId    the id of the cm handle for the susbcription
98      * @param xpath         the target xpath
99      * @return collection of subscription ids of ongoing cm notification subscription
100      */
101     public Collection<String> getOngoingCmSubscriptionIds(final DatastoreType datastoreType,
102             final String cmHandleId, final String xpath) {
103
104         final String isOngoingCmSubscriptionCpsPathQuery =
105                 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted(
106                         datastoreType.getDatastoreName(), cmHandleId, escapeQuotesByDoublingThem(xpath));
107         final Collection<DataNode> existingNodes =
108                 cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME,
109                         isOngoingCmSubscriptionCpsPathQuery, OMIT_DESCENDANTS);
110         if (existingNodes.isEmpty()) {
111             return Collections.emptyList();
112         }
113         return (List<String>) existingNodes.iterator().next().getLeaves().get("subscriptionIds");
114     }
115
116     /**
117      * Add cm notification subscription.
118      *
119      * @param datastoreType     the susbcription target datastore type
120      * @param cmHandleId        the id of the cm handle for the susbcription
121      * @param xpath             the target xpath
122      * @param newSubscriptionId subscription id to be added
123      */
124     public void addCmSubscription(final DatastoreType datastoreType, final String cmHandleId,
125             final String xpath, final String newSubscriptionId) {
126         final Collection<String> subscriptionIds =
127                 getOngoingCmSubscriptionIds(datastoreType, cmHandleId, xpath);
128         if (subscriptionIds.isEmpty()) {
129             addFirstSubscriptionForDatastoreCmHandleAndXpath(datastoreType, cmHandleId, xpath, newSubscriptionId);
130         } else if (!subscriptionIds.contains(newSubscriptionId)) {
131             subscriptionIds.add(newSubscriptionId);
132             saveSubscriptionDetails(datastoreType, cmHandleId, xpath, subscriptionIds);
133         }
134     }
135
136     /**
137      * Remove cm notification Subscription.
138      *
139      * @param datastoreType  the susbcription target datastore type
140      * @param cmHandleId     the id of the cm handle for the susbcription
141      * @param xpath          the target xpath
142      * @param subscriptionId subscription id to remove
143      */
144     public void removeCmSubscription(final DatastoreType datastoreType, final String cmHandleId,
145             final String xpath, final String subscriptionId) {
146         final Collection<String> subscriptionIds =
147                 getOngoingCmSubscriptionIds(datastoreType, cmHandleId, xpath);
148         if (subscriptionIds.remove(subscriptionId)) {
149             saveSubscriptionDetails(datastoreType, cmHandleId, xpath, subscriptionIds);
150             log.info("There are subscribers left for the following cps path {} :",
151                     CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted(
152                             datastoreType.getDatastoreName(), cmHandleId, escapeQuotesByDoublingThem(xpath)));
153             if (subscriptionIds.isEmpty()) {
154                 log.info("No subscribers left for the following cps path {} :",
155                         CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted(
156                                 datastoreType.getDatastoreName(), cmHandleId, escapeQuotesByDoublingThem(xpath)));
157                 deleteListOfSubscriptionsFor(datastoreType, cmHandleId, xpath);
158             }
159         }
160     }
161
162     private void deleteListOfSubscriptionsFor(final DatastoreType datastoreType, final String cmHandleId,
163             final String xpath) {
164         cpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME,
165                 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted(
166                         datastoreType.getDatastoreName(), cmHandleId, escapeQuotesByDoublingThem(xpath)),
167                 OffsetDateTime.now());
168         final Collection<DataNode> existingFiltersForCmHandle =
169                 cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME,
170                                 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE.formatted(
171                                         datastoreType.getDatastoreName(), cmHandleId),
172                                 DIRECT_CHILDREN_ONLY).iterator().next()
173                         .getChildDataNodes();
174         if (existingFiltersForCmHandle.isEmpty()) {
175             removeCmHandleFromDatastore(datastoreType.getDatastoreName(), cmHandleId);
176         }
177     }
178
179     private void removeCmHandleFromDatastore(final String datastoreName, final String cmHandleId) {
180         cpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME,
181                 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE.formatted(datastoreName, cmHandleId),
182                 OffsetDateTime.now());
183     }
184
185     private boolean isFirstSubscriptionForCmHandle(final DatastoreType datastoreType, final String cmHandleId) {
186         return cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME,
187                 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE.formatted(
188                         datastoreType.getDatastoreName(), cmHandleId), OMIT_DESCENDANTS).isEmpty();
189     }
190
191     private void addFirstSubscriptionForDatastoreCmHandleAndXpath(final DatastoreType datastoreType,
192             final String cmHandleId, final String xpath, final String subscriptionId) {
193         final Collection<String> newSubscriptionList = Collections.singletonList(subscriptionId);
194         final String subscriptionDetailsAsJson = getSubscriptionDetailsAsJson(xpath, newSubscriptionList);
195         if (isFirstSubscriptionForCmHandle(datastoreType, cmHandleId)) {
196             final String parentXpath =
197                     "/datastores/datastore[@name='%s']/cm-handles".formatted(datastoreType.getDatastoreName());
198             final String subscriptionAsJson =
199                     String.format("{\"cm-handle\":[{\"id\":\"%s\",\"filters\":%s}]}", cmHandleId,
200                             subscriptionDetailsAsJson);
201             cpsDataService.saveData(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, parentXpath, subscriptionAsJson,
202                     OffsetDateTime.now(), ContentType.JSON);
203         } else {
204             cpsDataService.saveListElements(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME,
205                     CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE.formatted(
206                             datastoreType.getDatastoreName(), cmHandleId), subscriptionDetailsAsJson,
207                     OffsetDateTime.now());
208         }
209     }
210
211     private void saveSubscriptionDetails(final DatastoreType datastoreType, final String cmHandleId, final String xpath,
212             final Collection<String> subscriptionIds) {
213         final String subscriptionDetailsAsJson = getSubscriptionDetailsAsJson(xpath, subscriptionIds);
214         cpsDataService.updateNodeLeaves(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME,
215                 CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE.formatted(
216                         datastoreType.getDatastoreName(), cmHandleId), subscriptionDetailsAsJson, OffsetDateTime.now(),
217                 ContentType.JSON);
218     }
219
220     private String getSubscriptionDetailsAsJson(final String xpath, final Collection<String> subscriptionIds) {
221         final Map<String, Serializable> subscriptionDetailsAsMap =
222                 Map.of("xpath", xpath, "subscriptionIds", (Serializable) subscriptionIds);
223         return "{\"filter\":[" + jsonObjectMapper.asJsonString(subscriptionDetailsAsMap) + "]}";
224     }
225
226     private static String escapeQuotesByDoublingThem(final String inputXpath) {
227         return inputXpath.replace("'", "''");
228     }
229
230 }
231