NONRTRIC - A scheme for versioning of types.
[nonrtric/plt/informationcoordinatorservice.git] / src / main / java / org / oransc / ics / controllers / r1producer / ProducerController.java
1 /*-
2  * ========================LICENSE_START=================================
3  * O-RAN-SC
4  * %%
5  * Copyright (C) 2020 Nordix Foundation
6  * %%
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ========================LICENSE_END===================================
19  */
20
21 package org.oransc.ics.controllers.r1producer;
22
23 import com.google.gson.Gson;
24 import com.google.gson.GsonBuilder;
25
26 import io.swagger.v3.oas.annotations.Operation;
27 import io.swagger.v3.oas.annotations.Parameter;
28 import io.swagger.v3.oas.annotations.media.ArraySchema;
29 import io.swagger.v3.oas.annotations.media.Content;
30 import io.swagger.v3.oas.annotations.media.Schema;
31 import io.swagger.v3.oas.annotations.responses.ApiResponse;
32 import io.swagger.v3.oas.annotations.responses.ApiResponses;
33 import io.swagger.v3.oas.annotations.tags.Tag;
34
35 import java.lang.invoke.MethodHandles;
36 import java.net.URI;
37 import java.net.URISyntaxException;
38 import java.util.ArrayList;
39 import java.util.Collection;
40 import java.util.HashSet;
41 import java.util.List;
42 import java.util.Set;
43
44 import org.oransc.ics.controllers.ErrorResponse;
45 import org.oransc.ics.controllers.VoidResponse;
46 import org.oransc.ics.exceptions.ServiceException;
47 import org.oransc.ics.repository.InfoJob;
48 import org.oransc.ics.repository.InfoJobs;
49 import org.oransc.ics.repository.InfoProducer;
50 import org.oransc.ics.repository.InfoProducerRegistrationInfo;
51 import org.oransc.ics.repository.InfoProducers;
52 import org.oransc.ics.repository.InfoType;
53 import org.oransc.ics.repository.InfoTypeSubscriptions;
54 import org.oransc.ics.repository.InfoTypes;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57 import org.springframework.beans.factory.annotation.Autowired;
58 import org.springframework.http.HttpStatus;
59 import org.springframework.http.MediaType;
60 import org.springframework.http.ResponseEntity;
61 import org.springframework.web.bind.annotation.DeleteMapping;
62 import org.springframework.web.bind.annotation.GetMapping;
63 import org.springframework.web.bind.annotation.PathVariable;
64 import org.springframework.web.bind.annotation.PutMapping;
65 import org.springframework.web.bind.annotation.RequestBody;
66 import org.springframework.web.bind.annotation.RequestParam;
67 import org.springframework.web.bind.annotation.RestController;
68
69 @SuppressWarnings("squid:S2629") // Invoke method(s) only conditionally
70 @RestController("Producer registry")
71 @Tag(name = ProducerConsts.PRODUCER_API_NAME, description = ProducerConsts.PRODUCER_API_DESCRIPTION)
72 public class ProducerController {
73
74     private static Gson gson = new GsonBuilder().create();
75     private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
76
77     @Autowired
78     private InfoJobs infoJobs;
79
80     @Autowired
81     private InfoTypes infoTypes;
82
83     @Autowired
84     private InfoProducers infoProducers;
85
86     @Autowired
87     private InfoTypeSubscriptions typeSubscriptions;
88
89     @GetMapping(path = ProducerConsts.API_ROOT + "/info-types", produces = MediaType.APPLICATION_JSON_VALUE) //
90     @Operation(summary = "Info Type identifiers", description = "") //
91     @ApiResponses(
92         value = { //
93             @ApiResponse(
94                 responseCode = "200",
95                 description = "Info Type identifiers", //
96                 content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))) //
97         })
98     public ResponseEntity<Object> getInfoTypdentifiers( //
99     ) {
100         logger.debug("GET info type identifiers");
101         List<String> result = new ArrayList<>();
102         for (InfoType infoType : this.infoTypes.getAllInfoTypes()) {
103             result.add(infoType.getId());
104         }
105
106         return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
107     }
108
109     @GetMapping(
110         path = ProducerConsts.API_ROOT + "/info-types/{infoTypeId}",
111         produces = MediaType.APPLICATION_JSON_VALUE)
112     @Operation(summary = "Individual Information Type", description = "")
113     @ApiResponses(
114         value = { //
115             @ApiResponse(
116                 responseCode = "200",
117                 description = "Info Type", //
118                 content = @Content(schema = @Schema(implementation = ProducerInfoTypeInfo.class))), //
119             @ApiResponse(
120                 responseCode = "404",
121                 description = "Information type is not found", //
122                 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class)))})
123     public ResponseEntity<Object> getInfoType( //
124         @PathVariable(ProducerConsts.INFO_TYPE_ID_PATH) String infoTypeId) {
125         try {
126             logger.debug("GET info type, infoTypeId: {}", infoTypeId);
127             InfoType t = this.infoTypes.getType(infoTypeId);
128             ProducerInfoTypeInfo info = toInfoTypeInfo(t);
129             return new ResponseEntity<>(gson.toJson(info), HttpStatus.OK);
130         } catch (Exception e) {
131             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
132         }
133     }
134
135     @PutMapping(
136         path = ProducerConsts.API_ROOT + "/info-types/{infoTypeId}",
137         produces = MediaType.APPLICATION_JSON_VALUE)
138     @ApiResponses(
139         value = { //
140             @ApiResponse(
141                 responseCode = "200",
142                 description = "Type updated", //
143                 content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
144             @ApiResponse(
145                 responseCode = "201",
146                 description = "Type created", //
147                 content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
148             @ApiResponse(
149                 responseCode = "400",
150                 description = "Input validation failed", //
151                 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class)))})
152     @Operation(summary = "Individual Information Type", description = "")
153     public ResponseEntity<Object> putInfoType( //
154         @PathVariable(ProducerConsts.INFO_TYPE_ID_PATH) String infoTypeId, //
155         @RequestBody ProducerInfoTypeInfo registrationInfo) {
156
157         logger.debug("PUT info type, infoTypeId: {}, info: {}", infoTypeId, registrationInfo);
158
159         InfoType previousDefinition = this.infoTypes.get(infoTypeId);
160         if (registrationInfo.jobDataSchema == null) {
161             return ErrorResponse.create("No schema provided", HttpStatus.BAD_REQUEST);
162         }
163         InfoType newDefinition =
164             new InfoType(infoTypeId, registrationInfo.jobDataSchema, registrationInfo.typeSpecificInformation);
165         this.infoTypes.put(newDefinition);
166         this.typeSubscriptions.notifyTypeRegistered(newDefinition);
167         return new ResponseEntity<>(previousDefinition == null ? HttpStatus.CREATED : HttpStatus.OK);
168     }
169
170     @DeleteMapping(
171         path = ProducerConsts.API_ROOT + "/info-types/{infoTypeId}",
172         produces = MediaType.APPLICATION_JSON_VALUE) //
173     @Operation(summary = "Individual Information Type", description = ProducerConsts.DELETE_INFO_TYPE_DESCRPTION) //
174     @ApiResponses(
175         value = { //
176             @ApiResponse(
177                 responseCode = "200",
178                 description = "Not used", //
179                 content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
180             @ApiResponse(
181                 responseCode = "204",
182                 description = "Producer deleted", //
183                 content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
184             @ApiResponse(
185                 responseCode = "404",
186                 description = "Information type is not found", //
187                 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))), //
188             @ApiResponse(
189                 responseCode = "409",
190                 description = "The Information type has one or several active producers", //
191                 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
192         })
193     public ResponseEntity<Object> deleteInfoType( //
194         @PathVariable(ProducerConsts.INFO_TYPE_ID_PATH) String infoTypeId) {
195
196         logger.debug("DELETE info type, infoTypeId: {}", infoTypeId);
197         InfoType type = this.infoTypes.get(infoTypeId);
198         if (type == null) {
199             return ErrorResponse.create("Information type not found", HttpStatus.NOT_FOUND);
200         }
201         if (!this.infoProducers.getProducersSupportingType(type).isEmpty()) {
202             String firstProducerId = this.infoProducers.getProducersSupportingType(type).iterator().next().getId();
203             return ErrorResponse.create("The type has active producers: " + firstProducerId, HttpStatus.CONFLICT);
204         }
205         this.infoTypes.remove(type);
206         infoJobs.getJobsForType(type).forEach(job -> infoJobs.remove(job, infoProducers)); // Delete jobs for the
207                                                                                            // type
208         this.typeSubscriptions.notifyTypeRemoved(type);
209         return new ResponseEntity<>(HttpStatus.NO_CONTENT);
210     }
211
212     @GetMapping(path = ProducerConsts.API_ROOT + "/info-producers", produces = MediaType.APPLICATION_JSON_VALUE)
213     @Operation(summary = "Information producer identifiers", description = "")
214     @ApiResponses(
215         value = { //
216             @ApiResponse(
217                 responseCode = "200",
218                 description = "Information producer identifiers", //
219                 content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))) //
220         })
221     public ResponseEntity<Object> getInfoProducerIdentifiers( //
222         @Parameter(
223             name = ProducerConsts.INFO_TYPE_ID_PARAM,
224             required = false,
225             description = "If given, only the producers for the Info Type is returned.") //
226         @RequestParam(name = ProducerConsts.INFO_TYPE_ID_PARAM, required = false) String typeId //
227     ) throws ServiceException {
228         logger.debug("GET producer identifiers");
229         List<String> result = new ArrayList<>();
230         for (InfoProducer infoProducer : getProducers(typeId)) {
231             result.add(infoProducer.getId());
232         }
233
234         return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
235     }
236
237     private Collection<InfoProducer> getProducers(String typeId) throws ServiceException {
238         if (typeId == null) {
239             return this.infoProducers.getAllProducers();
240         }
241         InfoType type = infoTypes.get(typeId);
242         if (type == null) {
243             return new ArrayList<>();
244         }
245         return infoProducers.getProducersSupportingType(infoTypes.getType(typeId));
246     }
247
248     @GetMapping(
249         path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}",
250         produces = MediaType.APPLICATION_JSON_VALUE)
251     @Operation(summary = "Individual Information Producer", description = "")
252     @ApiResponses(
253         value = { //
254             @ApiResponse(
255                 responseCode = "200",
256                 description = "Information producer", //
257                 content = @Content(schema = @Schema(implementation = ProducerRegistrationInfo.class))), //
258             @ApiResponse(
259                 responseCode = "404",
260                 description = "Information producer is not found", //
261                 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class)))//
262         })
263     public ResponseEntity<Object> getInfoProducer( //
264         @PathVariable(ProducerConsts.INFO_PRODUCER_ID_PATH) String infoProducerId) {
265         try {
266             logger.debug("GET info producer, infoProducerId: {}", infoProducerId);
267             InfoProducer producer = this.infoProducers.getProducer(infoProducerId);
268             ProducerRegistrationInfo info = toProducerRegistrationInfo(producer);
269             return new ResponseEntity<>(gson.toJson(info), HttpStatus.OK);
270         } catch (Exception e) {
271             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
272         }
273     }
274
275     @GetMapping(
276         path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}/info-jobs",
277         produces = MediaType.APPLICATION_JSON_VALUE)
278     @Operation(
279         summary = "Information Job definitions",
280         description = "Information Job definitions for one Information Producer")
281     @ApiResponses(
282         value = { //
283             @ApiResponse(
284                 responseCode = "404",
285                 description = "Information producer is not found", //
286                 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))), //
287             @ApiResponse(
288                 responseCode = "200",
289                 description = "Information producer", //
290                 content = @Content(array = @ArraySchema(schema = @Schema(implementation = ProducerJobInfo.class)))), //
291         })
292     public ResponseEntity<Object> getInfoProducerJobs( //
293         @PathVariable(ProducerConsts.INFO_PRODUCER_ID_PATH) String infoProducerId) {
294         try {
295             logger.debug("GET info producer, infoProducerId: {}", infoProducerId);
296             InfoProducer producer = this.infoProducers.getProducer(infoProducerId);
297             Collection<ProducerJobInfo> producerJobs = new ArrayList<>();
298             for (InfoType type : producer.getInfoTypes()) {
299                 for (InfoJob infoJob : this.infoJobs.getJobsForType(type)) {
300                     ProducerJobInfo request = new ProducerJobInfo(infoJob);
301                     producerJobs.add(request);
302                 }
303             }
304
305             return new ResponseEntity<>(gson.toJson(producerJobs), HttpStatus.OK);
306         } catch (Exception e) {
307             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
308         }
309     }
310
311     @GetMapping(
312         path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}/status",
313         produces = MediaType.APPLICATION_JSON_VALUE) //
314     @Operation(summary = "Information producer status") //
315     @ApiResponses(
316         value = { //
317             @ApiResponse(
318                 responseCode = "200",
319                 description = "Information producer status", //
320                 content = @Content(schema = @Schema(implementation = ProducerStatusInfo.class))), //
321             @ApiResponse(
322                 responseCode = "404",
323                 description = "Information producer is not found", //
324                 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
325         })
326     public ResponseEntity<Object> getInfoProducerStatus( //
327         @PathVariable(ProducerConsts.INFO_PRODUCER_ID_PATH) String infoProducerId) {
328         try {
329             logger.debug("GET producer status, infoProducerId: {}", infoProducerId);
330             InfoProducer producer = this.infoProducers.getProducer(infoProducerId);
331             return new ResponseEntity<>(gson.toJson(producerStatusInfo(producer)), HttpStatus.OK);
332         } catch (Exception e) {
333             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
334         }
335     }
336
337     private ProducerStatusInfo producerStatusInfo(InfoProducer producer) {
338         var opState = producer.isAvailable() ? ProducerStatusInfo.OperationalState.ENABLED
339             : ProducerStatusInfo.OperationalState.DISABLED;
340         return new ProducerStatusInfo(opState);
341     }
342
343     @PutMapping(
344         path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}", //
345         produces = MediaType.APPLICATION_JSON_VALUE)
346     @Operation(summary = "Individual Information Producer", description = "")
347     @ApiResponses(
348         value = { //
349             @ApiResponse(
350                 responseCode = "201",
351                 description = "Producer created", //
352                 content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
353             @ApiResponse(
354                 responseCode = "200",
355                 description = "Producer updated", //
356                 content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
357             @ApiResponse(
358                 responseCode = "404",
359                 description = "Producer type not found", //
360                 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))), //
361             @ApiResponse(
362                 responseCode = "400",
363                 description = "Input validation failed", //
364                 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
365         })
366     public ResponseEntity<Object> putInfoProducer( //
367         @PathVariable("infoProducerId") String infoProducerId, //
368         @RequestBody ProducerRegistrationInfo registrationInfo) {
369         try {
370             logger.debug("PUT info producer, infoProducerId: {}, body: {}", infoProducerId, registrationInfo);
371             validateUri(registrationInfo.jobCallbackUrl);
372             validateUri(registrationInfo.producerSupervisionCallbackUrl);
373             InfoProducer previousDefinition = this.infoProducers.get(infoProducerId);
374             this.infoProducers.registerProducer(toProducerRegistrationInfo(infoProducerId, registrationInfo));
375             return new ResponseEntity<>(previousDefinition == null ? HttpStatus.CREATED : HttpStatus.OK);
376         } catch (ServiceException e) {
377             return ErrorResponse.create(e, e.getHttpStatus());
378         }
379     }
380
381     private void validateUri(String url) throws ServiceException {
382         if (url != null && !url.isEmpty()) {
383             try {
384                 URI uri = new URI(url);
385                 if (!uri.isAbsolute()) {
386                     throw new ServiceException("URI: " + url + " is not absolute", HttpStatus.BAD_REQUEST);
387                 }
388             } catch (URISyntaxException e) {
389                 throw new ServiceException(e.getMessage(), HttpStatus.BAD_REQUEST);
390             }
391         } else {
392             throw new ServiceException("Missing required URL", HttpStatus.BAD_REQUEST);
393         }
394     }
395
396     @DeleteMapping(
397         path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}",
398         produces = MediaType.APPLICATION_JSON_VALUE)
399     @Operation(summary = "Individual Information Producer", description = "")
400     @ApiResponses(
401         value = { //
402             @ApiResponse(
403                 responseCode = "200",
404                 description = "Not used", //
405                 content = @Content(schema = @Schema(implementation = VoidResponse.class))),
406             @ApiResponse(
407                 responseCode = "204",
408                 description = "Producer deleted", //
409                 content = @Content(schema = @Schema(implementation = VoidResponse.class))),
410             @ApiResponse(
411                 responseCode = "404",
412                 description = "Producer is not found", //
413                 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
414         })
415     public ResponseEntity<Object> deleteInfoProducer(
416         @PathVariable(ProducerConsts.INFO_PRODUCER_ID_PATH) String infoProducerId) {
417         try {
418             logger.debug("DELETE info producer, infoProducerId: {}", infoProducerId);
419             final InfoProducer producer = this.infoProducers.getProducer(infoProducerId);
420             this.infoProducers.deregisterProducer(producer);
421             return new ResponseEntity<>(HttpStatus.NO_CONTENT);
422         } catch (ServiceException e) {
423             return ErrorResponse.create(e, e.getHttpStatus());
424         }
425     }
426
427     private ProducerRegistrationInfo toProducerRegistrationInfo(InfoProducer p) {
428         Collection<String> types = new ArrayList<>();
429         for (InfoType type : p.getInfoTypes()) {
430             types.add(type.getId());
431         }
432         return new ProducerRegistrationInfo(types, p.getJobCallbackUrl(), p.getProducerSupervisionCallbackUrl());
433     }
434
435     private ProducerInfoTypeInfo toInfoTypeInfo(InfoType t) {
436         return new ProducerInfoTypeInfo(t.getJobDataSchema(), t.getTypeSpecificInfo());
437     }
438
439     private InfoProducerRegistrationInfo toProducerRegistrationInfo(String infoProducerId,
440         ProducerRegistrationInfo info) throws ServiceException {
441         Set<InfoType> supportedTypes = new HashSet<>();
442         for (String typeId : info.supportedTypeIds) {
443             InfoType type = this.infoTypes.getType(typeId);
444             supportedTypes.add(type);
445         }
446
447         return InfoProducerRegistrationInfo.builder() //
448             .id(infoProducerId) //
449             .jobCallbackUrl(info.jobCallbackUrl) //
450             .producerSupervisionCallbackUrl(info.producerSupervisionCallbackUrl) //
451             .supportedTypes(supportedTypes) //
452             .build();
453     }
454 }