2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2023-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.dmi.rest.stub.controller;
24 import com.fasterxml.jackson.core.JsonProcessingException;
25 import com.fasterxml.jackson.databind.JsonNode;
26 import com.fasterxml.jackson.databind.ObjectMapper;
27 import io.cloudevents.CloudEvent;
28 import io.cloudevents.core.builder.CloudEventBuilder;
30 import java.util.ArrayList;
31 import java.util.HashMap;
32 import java.util.List;
34 import java.util.UUID;
35 import java.util.concurrent.atomic.AtomicInteger;
36 import java.util.stream.Collectors;
37 import lombok.RequiredArgsConstructor;
38 import lombok.extern.slf4j.Slf4j;
39 import org.json.simple.parser.JSONParser;
40 import org.json.simple.parser.ParseException;
41 import org.onap.cps.ncmp.dmi.datajobs.model.SubjobWriteRequest;
42 import org.onap.cps.ncmp.dmi.datajobs.model.SubjobWriteResponse;
43 import org.onap.cps.ncmp.dmi.rest.stub.model.data.operational.DataOperationRequest;
44 import org.onap.cps.ncmp.dmi.rest.stub.model.data.operational.DmiDataOperationRequest;
45 import org.onap.cps.ncmp.dmi.rest.stub.model.data.operational.DmiOperationCmHandle;
46 import org.onap.cps.ncmp.dmi.rest.stub.utils.EventDateTimeFormatter;
47 import org.onap.cps.ncmp.dmi.rest.stub.utils.ResourceFileReaderUtil;
48 import org.onap.cps.ncmp.events.async1_0_0.Data;
49 import org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent;
50 import org.onap.cps.ncmp.events.async1_0_0.Response;
51 import org.springframework.beans.factory.annotation.Value;
52 import org.springframework.context.ApplicationContext;
53 import org.springframework.core.io.Resource;
54 import org.springframework.core.io.ResourceLoader;
55 import org.springframework.http.HttpStatus;
56 import org.springframework.http.ResponseEntity;
57 import org.springframework.kafka.core.KafkaTemplate;
58 import org.springframework.web.bind.annotation.DeleteMapping;
59 import org.springframework.web.bind.annotation.GetMapping;
60 import org.springframework.web.bind.annotation.PathVariable;
61 import org.springframework.web.bind.annotation.PostMapping;
62 import org.springframework.web.bind.annotation.PutMapping;
63 import org.springframework.web.bind.annotation.RequestBody;
64 import org.springframework.web.bind.annotation.RequestHeader;
65 import org.springframework.web.bind.annotation.RequestMapping;
66 import org.springframework.web.bind.annotation.RequestParam;
67 import org.springframework.web.bind.annotation.RestController;
70 @RequestMapping("${rest.api.dmi-stub-base-path}")
72 @RequiredArgsConstructor
73 public class DmiRestStubController {
75 private static final String DEFAULT_TAG = "tagD";
76 private static final String DEFAULT_PASSTHROUGH_OPERATION = "read";
77 private static final String dataOperationEventType = "org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent";
78 private static final Map<String, String> moduleSetTagPerCmHandleId = new HashMap<>();
79 private final KafkaTemplate<String, CloudEvent> cloudEventKafkaTemplate;
80 private final ObjectMapper objectMapper;
81 private final ApplicationContext applicationContext;
82 @Value("${app.ncmp.async-m2m.topic}")
83 private String ncmpAsyncM2mTopic;
84 @Value("${delay.module-references-delay-ms}")
85 private long moduleReferencesDelayMs;
86 @Value("${delay.module-resources-delay-ms}")
87 private long moduleResourcesDelayMs;
88 @Value("${delay.read-data-for-cm-handle-delay-ms}")
89 private long readDataForCmHandleDelayMs;
90 @Value("${delay.write-data-for-cm-handle-delay-ms}")
91 private long writeDataForCmHandleDelayMs;
92 private final AtomicInteger subJobWriteRequestCounter = new AtomicInteger();
95 * This code defines a REST API endpoint for adding new the module set tag mapping. The endpoint receives the
96 * cmHandleId and moduleSetTag as request body and add into moduleSetTagPerCmHandleId map with the provided
99 * @param requestBody map of cmHandleId and moduleSetTag
100 * @return a ResponseEntity object containing the updated moduleSetTagPerCmHandleId map as the response body
102 @PostMapping("/v1/tagMapping")
103 public ResponseEntity<Map<String, String>> addTagForMapping(@RequestBody final Map<String, String> requestBody) {
104 moduleSetTagPerCmHandleId.putAll(requestBody);
105 return new ResponseEntity<>(requestBody, HttpStatus.CREATED);
109 * This code defines a GET endpoint of module set tag mapping.
111 * @return The map represents the module set tag mapping.
113 @GetMapping("/v1/tagMapping")
114 public ResponseEntity<Map<String, String>> getTagMapping() {
115 return ResponseEntity.ok(moduleSetTagPerCmHandleId);
119 * This code defines a GET endpoint of module set tag by cm handle ID.
121 * @return The map represents the module set tag mapping filtered by cm handle ID.
123 @GetMapping("/v1/tagMapping/ch/{cmHandleId}")
124 public ResponseEntity<String> getTagMappingByCmHandleId(@PathVariable final String cmHandleId) {
125 return ResponseEntity.ok(moduleSetTagPerCmHandleId.get(cmHandleId));
129 * This code defines a REST API endpoint for updating the module set tag mapping. The endpoint receives the
130 * cmHandleId and moduleSetTag as request body and updates the moduleSetTagPerCmHandleId map with the provided
133 * @param requestBody map of cmHandleId and moduleSetTag
134 * @return a ResponseEntity object containing the updated moduleSetTagPerCmHandleId map as the response body
137 @PutMapping("/v1/tagMapping")
138 public ResponseEntity<Map<String, String>> updateTagMapping(@RequestBody final Map<String, String> requestBody) {
139 moduleSetTagPerCmHandleId.putAll(requestBody);
140 return ResponseEntity.noContent().build();
144 * It contains a method to delete an entry from the moduleSetTagPerCmHandleId map.
145 * The method takes a cmHandleId as a parameter and removes the corresponding entry from the map.
147 * @return a ResponseEntity containing the updated map.
149 @DeleteMapping("/v1/tagMapping/ch/{cmHandleId}")
150 public ResponseEntity<String> deleteTagMappingByCmHandleId(@PathVariable final String cmHandleId) {
151 moduleSetTagPerCmHandleId.remove(cmHandleId);
152 return ResponseEntity.ok(String.format("Mapping of %s is deleted successfully", cmHandleId));
156 * Get all modules for given cm handle.
158 * @param cmHandleId The identifier for a network function, network element, subnetwork,
159 * or any other cm object by managed Network CM Proxy
160 * @param moduleReferencesRequest module references request body
161 * @return ResponseEntity response entity having module response as json string.
163 @PostMapping("/v1/ch/{cmHandleId}/modules")
164 public ResponseEntity<String> getModuleReferences(@PathVariable("cmHandleId") final String cmHandleId,
165 @RequestBody final Object moduleReferencesRequest) {
166 delay(moduleReferencesDelayMs);
168 log.info("Incoming DMI request body: {}",
169 objectMapper.writeValueAsString(moduleReferencesRequest));
170 } catch (final JsonProcessingException jsonProcessingException) {
171 log.info("Unable to parse dmi data operation request to json string");
173 final String moduleResponseContent = getModuleResourceResponse(cmHandleId,
174 "ModuleResponse.json");
175 log.info("cm handle: {} requested for modules", cmHandleId);
176 return ResponseEntity.ok(moduleResponseContent);
180 * Retrieves module resources for a given cmHandleId.
182 * @param cmHandleId The identifier for a network function, network element, subnetwork,
183 * or any other cm object by managed Network CM Proxy
184 * @param moduleResourcesReadRequest module resources read request body
185 * @return ResponseEntity response entity having module resources response as json string.
187 @PostMapping("/v1/ch/{cmHandleId}/moduleResources")
188 public ResponseEntity<String> retrieveModuleResources(
189 @PathVariable("cmHandleId") final String cmHandleId,
190 @RequestBody final Object moduleResourcesReadRequest) {
191 delay(moduleResourcesDelayMs);
192 final String moduleResourcesResponseContent = getModuleResourceResponse(cmHandleId,
193 "ModuleResourcesResponse.json");
194 log.info("cm handle: {} requested for modules resources", cmHandleId);
195 return ResponseEntity.ok(moduleResourcesResponseContent);
199 * Create resource data from passthrough operational or running for a cm handle.
201 * @param cmHandleId The identifier for a network function, network element, subnetwork,
202 * or any other cm object by managed Network CM Proxy
203 * @param datastoreName datastore name
204 * @param resourceIdentifier resource identifier
205 * @param options options
206 * @param topic client given topic name
207 * @return (@ code ResponseEntity) response entity
209 @PostMapping("/v1/ch/{cmHandleId}/data/ds/{datastoreName}")
210 public ResponseEntity<String> getResourceDataForCmHandle(
211 @PathVariable("cmHandleId") final String cmHandleId,
212 @PathVariable("datastoreName") final String datastoreName,
213 @RequestParam(value = "resourceIdentifier") final String resourceIdentifier,
214 @RequestParam(value = "options", required = false) final String options,
215 @RequestParam(value = "topic", required = false) final String topic,
216 @RequestHeader(value = "Authorization", required = false) final String authorization,
217 @RequestBody final String requestBody) {
218 log.info("DMI AUTH HEADER: {}", authorization);
219 final String passthroughOperationType = getPassthroughOperationType(requestBody);
220 if (passthroughOperationType.equals("read")) {
221 delay(readDataForCmHandleDelayMs);
223 delay(writeDataForCmHandleDelayMs);
225 log.info("Logging request body {}", requestBody);
227 final String sampleJson = ResourceFileReaderUtil.getResourceFileContent(applicationContext.getResource(
228 ResourceLoader.CLASSPATH_URL_PREFIX + "data/operational/ietf-network-topology-sample-rfc8345.json"));
229 return ResponseEntity.ok(sampleJson);
233 * This method is not implemented for ONAP DMI plugin.
235 * @param topic client given topic name
236 * @param requestId requestId generated by NCMP as an ack for client
237 * @param dmiDataOperationRequest list of operation details
238 * @return (@ code ResponseEntity) response entity
240 @PostMapping("/v1/data")
241 public ResponseEntity<Void> getResourceDataForCmHandleDataOperation(
242 @RequestParam(value = "topic") final String topic,
243 @RequestParam(value = "requestId") final String requestId,
244 @RequestBody final DmiDataOperationRequest dmiDataOperationRequest) {
245 delay(writeDataForCmHandleDelayMs);
247 log.info("Request received from the NCMP to DMI Plugin: {}",
248 objectMapper.writeValueAsString(dmiDataOperationRequest));
249 } catch (final JsonProcessingException jsonProcessingException) {
250 log.info("Unable to process dmi data operation request to json string");
252 dmiDataOperationRequest.getOperations().forEach(dmiDataOperation -> {
253 final DataOperationEvent dataOperationEvent = getDataOperationEvent(dmiDataOperation);
254 dmiDataOperation.getCmHandles().forEach(dmiOperationCmHandle -> {
255 log.info("Module Set Tag received: {}", dmiOperationCmHandle.getModuleSetTag());
256 dataOperationEvent.getData().getResponses().get(0).setIds(List.of(dmiOperationCmHandle.getId()));
257 final CloudEvent cloudEvent = buildAndGetCloudEvent(topic, requestId, dataOperationEvent);
258 cloudEventKafkaTemplate.send(ncmpAsyncM2mTopic, UUID.randomUUID().toString(), cloudEvent);
261 return new ResponseEntity<>(HttpStatus.ACCEPTED);
265 * Consume sub-job write requests from NCMP.
267 * @param requestId requestId generated by NCMP as an ack for client.
268 * @param subJobWriteRequest contains a collection of write requests and metadata.
269 * @return (@ code ResponseEntity) response for the write request.
271 @PostMapping("/v1/writeJob/{requestId}")
272 public ResponseEntity<SubjobWriteResponse> consumeWriteSubJobs(@PathVariable("requestId") final String requestId,
273 @RequestBody final SubjobWriteRequest subJobWriteRequest) {
274 log.debug("Request ID: {}", requestId);
275 log.debug("Request body: {}", subJobWriteRequest);
276 return ResponseEntity.ok(new SubjobWriteResponse(String.valueOf(subJobWriteRequestCounter.incrementAndGet()),
277 "some-dmi-service-name", "my-data-producer-id"));
281 * Retrieves the status of a given data job identified by {@code requestId} and {@code dataProducerJobId}.
283 * @param requestId Unique identifier for the outgoing request.
284 * @param dataProducerJobId Identifier of the data producer job.
285 * @return A ResponseEntity with HTTP status 200 (OK) and the data job's status as a string.
287 @GetMapping("/v1/dataJob/{requestId}/dataProducerJob/{dataProducerJobId}/status")
288 public ResponseEntity<String> retrieveDataJobStatus(
289 @PathVariable("requestId") final String requestId,
290 @PathVariable("dataProducerJobId") final String dataProducerJobId) {
291 log.info("Received request to retrieve data job status. Request ID: {}, Data Producer Job ID: {}",
292 requestId, dataProducerJobId);
293 return ResponseEntity.ok("FINISHED");
297 * Retrieves the result of a given data job identified by {@code requestId} and {@code dataProducerJobId}.
299 * @param requestId Identifier for the overall Datajob (required)
300 * @param dataProducerJobId Identifier for the data producer job (required)
301 * @param dataProducerId Identifier for the data producer as a query parameter (required)
302 * @param destination The destination of the results, Kafka topic name or s3 bucket name (required)
303 * @return A ResponseEntity with HTTP status 200 (OK) and the data job's result as an Object.
305 @GetMapping("/v1/dataJob/{requestId}/dataProducerJob/{dataProducerJobId}/result")
306 public ResponseEntity<Object> retrieveDataJobResult(
307 @PathVariable("requestId") final String requestId,
308 @PathVariable("dataProducerJobId") final String dataProducerJobId,
309 @RequestParam(name = "dataProducerId") String dataProducerId,
310 @RequestParam(name = "destination") String destination) {
311 log.debug("Received request to retrieve data job result. Request ID: {}, Data Producer Job ID: {}, " +
312 "Data Producer ID: {}, Destination: {}",
313 requestId, dataProducerJobId, dataProducerId, destination);
314 return ResponseEntity.ok(Map.of("result", "some status"));
317 private CloudEvent buildAndGetCloudEvent(final String topic, final String requestId,
318 final DataOperationEvent dataOperationEvent) {
319 CloudEvent cloudEvent = null;
321 cloudEvent = CloudEventBuilder.v1()
322 .withId(UUID.randomUUID().toString())
323 .withSource(URI.create("DMI"))
324 .withType(dataOperationEventType)
325 .withDataSchema(URI.create("urn:cps:" + dataOperationEventType + ":1.0.0"))
326 .withTime(EventDateTimeFormatter.toIsoOffsetDateTime(
327 EventDateTimeFormatter.getCurrentIsoFormattedDateTime()))
328 .withData(objectMapper.writeValueAsBytes(dataOperationEvent))
329 .withExtension("destination", topic)
330 .withExtension("correlationid", requestId)
332 } catch (final JsonProcessingException jsonProcessingException) {
333 log.error("Unable to parse event into bytes. cause : {}", jsonProcessingException.getMessage());
338 private DataOperationEvent getDataOperationEvent(final DataOperationRequest dataOperationRequest) {
339 final Response response = new Response();
341 response.setOperationId(dataOperationRequest.getOperationId());
342 response.setStatusCode("0");
343 response.setStatusMessage("Successfully applied changes");
344 response.setIds(dataOperationRequest.getCmHandles().stream().map(DmiOperationCmHandle::getId)
345 .collect(Collectors.toList()));
346 response.setResourceIdentifier(dataOperationRequest.getResourceIdentifier());
347 response.setOptions(dataOperationRequest.getOptions());
348 final String ietfNetworkTopologySample = ResourceFileReaderUtil.getResourceFileContent(
349 applicationContext.getResource(ResourceLoader.CLASSPATH_URL_PREFIX
350 + "data/operational/ietf-network-topology-sample-rfc8345.json"));
351 final JSONParser jsonParser = new JSONParser();
353 response.setResult(jsonParser.parse(ietfNetworkTopologySample));
354 } catch (final ParseException parseException) {
355 log.error("Unable to parse event result as json object. cause : {}", parseException.getMessage());
357 final List<Response> responseList = new ArrayList<>(1);
358 responseList.add(response);
359 final Data data = new Data();
360 data.setResponses(responseList);
361 final DataOperationEvent dataOperationEvent = new DataOperationEvent();
362 dataOperationEvent.setData(data);
363 return dataOperationEvent;
366 private String getModuleResourceResponse(final String cmHandleId, final String moduleResponseType) {
367 if (moduleSetTagPerCmHandleId.isEmpty()) {
368 log.info("Using default module responses of type ietfYang");
369 return ResourceFileReaderUtil.getResourceFileContent(applicationContext.getResource(
370 ResourceLoader.CLASSPATH_URL_PREFIX
371 + String.format("module/ietfYang-%s", moduleResponseType)));
373 final String moduleSetTag = moduleSetTagPerCmHandleId.getOrDefault(cmHandleId, DEFAULT_TAG);
374 final String moduleResponseFilePath = String.format("module/%s-%s", moduleSetTag, moduleResponseType);
375 final Resource moduleResponseResource = applicationContext.getResource(
376 ResourceLoader.CLASSPATH_URL_PREFIX + moduleResponseFilePath);
377 log.info("Using module responses from : {}", moduleResponseFilePath);
378 return ResourceFileReaderUtil.getResourceFileContent(moduleResponseResource);
381 private String getPassthroughOperationType(final String requestBody) {
383 final JsonNode rootNode = objectMapper.readTree(requestBody);
384 return rootNode.path("operation").asText();
385 } catch (final JsonProcessingException jsonProcessingException) {
386 log.error("Invalid JSON format. cause : {}", jsonProcessingException.getMessage());
388 return DEFAULT_PASSTHROUGH_OPERATION;
391 private void delay(final long milliseconds) {
393 Thread.sleep(milliseconds);
394 } catch (final InterruptedException e) {
395 log.error("Thread sleep interrupted: {}", e.getMessage());
396 Thread.currentThread().interrupt();