f87d012c50a17ce7d959d0af2a7726ad7eb7648c
[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.api.impl.events.cmsubscription.service;
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.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;
41
42 @Slf4j
43 @Service
44 @RequiredArgsConstructor
45 public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotificationSubscriptionPersistenceService {
46
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']
50             """.trim();
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";
53
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']";
56
57
58     private static final String CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_ID = """
59             //filter/subscriptionIds[text()='%s']
60             """.trim();
61
62     private final JsonObjectMapper jsonObjectMapper;
63     private final CpsQueryService cpsQueryService;
64     private final CpsDataService cpsDataService;
65
66     @Override
67     public boolean isOngoingCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
68                                                        final String xpath) {
69         return !getOngoingCmNotificationSubscriptionIds(datastoreType, cmHandleId, xpath).isEmpty();
70     }
71
72     @Override
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();
77     }
78
79     @Override
80     public Collection<String> getOngoingCmNotificationSubscriptionIds(final DatastoreType datastoreType,
81                                                                       final String cmHandleId, final String xpath) {
82
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();
91         }
92         return (List<String>) existingNodes.iterator().next().getLeaves().get("subscriptionIds");
93     }
94
95     @Override
96     public void addCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
97                                               final String xpath, final String subscriptionId) {
98         final Collection<String> subscriptionIds = getOngoingCmNotificationSubscriptionIds(datastoreType,
99                 cmHandleId, xpath);
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);
105         }
106     }
107
108     @Override
109     public void removeCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
110                                                  final String xpath, final String subscriptionId) {
111         final Collection<String> subscriptionIds = getOngoingCmNotificationSubscriptionIds(datastoreType,
112                 cmHandleId, xpath);
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);
123             }
124         }
125     }
126
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);
140         }
141     }
142
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());
147     }
148
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();
154     }
155
156     private void addFirstSubscriptionForDatastoreCmHandleAndXpath(final DatastoreType datastoreType,
157                                                                   final String cmHandleId,
158                                                                   final String xpath,
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);
169         } else {
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());
174         }
175     }
176
177     private void saveSubscriptionDetails(final DatastoreType datastoreType, final String cmHandleId,
178                                          final String xpath,
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());
184     }
185
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) + "]}";
190     }
191
192     private static String escapeQuotesByDoublingThem(final String inputXpath) {
193         return inputXpath.replace("'", "''");
194     }
195 }