2 * ========================LICENSE_START=================================
5 * Copyright (C) 2020 Nordix Foundation
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
11 * http://www.apache.org/licenses/LICENSE-2.0
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===================================
21 package org.oransc.ics.controllers.r1producer;
23 import com.google.gson.Gson;
24 import com.google.gson.GsonBuilder;
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;
35 import java.lang.invoke.MethodHandles;
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;
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;
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 {
74 private static Gson gson = new GsonBuilder().create();
75 private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
78 private InfoJobs infoJobs;
81 private InfoTypes infoTypes;
84 private InfoProducers infoProducers;
87 private InfoTypeSubscriptions typeSubscriptions;
89 @GetMapping(path = ProducerConsts.API_ROOT + "/info-types", produces = MediaType.APPLICATION_JSON_VALUE) //
90 @Operation(summary = "Info Type identifiers", description = "") //
95 description = "Info Type identifiers", //
96 content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))) //
98 public ResponseEntity<Object> getInfoTypdentifiers( //
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());
106 return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
110 path = ProducerConsts.API_ROOT + "/info-types/{infoTypeId}",
111 produces = MediaType.APPLICATION_JSON_VALUE)
112 @Operation(summary = "Individual Information Type", description = "")
116 responseCode = "200",
117 description = "Info Type", //
118 content = @Content(schema = @Schema(implementation = ProducerInfoTypeInfo.class))), //
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) {
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);
136 path = ProducerConsts.API_ROOT + "/info-types/{infoTypeId}",
137 produces = MediaType.APPLICATION_JSON_VALUE)
141 responseCode = "200",
142 description = "Type updated", //
143 content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
145 responseCode = "201",
146 description = "Type created", //
147 content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
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) {
157 logger.debug("PUT info type, infoTypeId: {}, info: {}", infoTypeId, registrationInfo);
159 InfoType previousDefinition = this.infoTypes.get(infoTypeId);
160 if (registrationInfo.jobDataSchema == null) {
161 return ErrorResponse.create("No schema provided", HttpStatus.BAD_REQUEST);
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);
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) //
177 responseCode = "200",
178 description = "Not used", //
179 content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
181 responseCode = "204",
182 description = "Producer deleted", //
183 content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
185 responseCode = "404",
186 description = "Information type is not found", //
187 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))), //
189 responseCode = "409",
190 description = "The Information type has one or several active producers", //
191 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
193 public ResponseEntity<Object> deleteInfoType( //
194 @PathVariable(ProducerConsts.INFO_TYPE_ID_PATH) String infoTypeId) {
196 logger.debug("DELETE info type, infoTypeId: {}", infoTypeId);
197 InfoType type = this.infoTypes.get(infoTypeId);
199 return ErrorResponse.create("Information type not found", HttpStatus.NOT_FOUND);
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);
205 this.infoTypes.remove(type);
206 infoJobs.getJobsForType(type).forEach(job -> infoJobs.remove(job, infoProducers)); // Delete jobs for the
208 this.typeSubscriptions.notifyTypeRemoved(type);
209 return new ResponseEntity<>(HttpStatus.NO_CONTENT);
212 @GetMapping(path = ProducerConsts.API_ROOT + "/info-producers", produces = MediaType.APPLICATION_JSON_VALUE)
213 @Operation(summary = "Information producer identifiers", description = "")
217 responseCode = "200",
218 description = "Information producer identifiers", //
219 content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))) //
221 public ResponseEntity<Object> getInfoProducerIdentifiers( //
223 name = ProducerConsts.INFO_TYPE_ID_PARAM,
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());
234 return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
237 private Collection<InfoProducer> getProducers(String typeId) throws ServiceException {
238 if (typeId == null) {
239 return this.infoProducers.getAllProducers();
241 InfoType type = infoTypes.get(typeId);
243 return new ArrayList<>();
245 return infoProducers.getProducersSupportingType(infoTypes.getType(typeId));
249 path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}",
250 produces = MediaType.APPLICATION_JSON_VALUE)
251 @Operation(summary = "Individual Information Producer", description = "")
255 responseCode = "200",
256 description = "Information producer", //
257 content = @Content(schema = @Schema(implementation = ProducerRegistrationInfo.class))), //
259 responseCode = "404",
260 description = "Information producer is not found", //
261 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class)))//
263 public ResponseEntity<Object> getInfoProducer( //
264 @PathVariable(ProducerConsts.INFO_PRODUCER_ID_PATH) String infoProducerId) {
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);
276 path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}/info-jobs",
277 produces = MediaType.APPLICATION_JSON_VALUE)
279 summary = "Information Job definitions",
280 description = "Information Job definitions for one Information Producer")
284 responseCode = "404",
285 description = "Information producer is not found", //
286 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))), //
288 responseCode = "200",
289 description = "Information producer", //
290 content = @Content(array = @ArraySchema(schema = @Schema(implementation = ProducerJobInfo.class)))), //
292 public ResponseEntity<Object> getInfoProducerJobs( //
293 @PathVariable(ProducerConsts.INFO_PRODUCER_ID_PATH) String infoProducerId) {
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);
305 return new ResponseEntity<>(gson.toJson(producerJobs), HttpStatus.OK);
306 } catch (Exception e) {
307 return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
312 path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}/status",
313 produces = MediaType.APPLICATION_JSON_VALUE) //
314 @Operation(summary = "Information producer status") //
318 responseCode = "200",
319 description = "Information producer status", //
320 content = @Content(schema = @Schema(implementation = ProducerStatusInfo.class))), //
322 responseCode = "404",
323 description = "Information producer is not found", //
324 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
326 public ResponseEntity<Object> getInfoProducerStatus( //
327 @PathVariable(ProducerConsts.INFO_PRODUCER_ID_PATH) String infoProducerId) {
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);
337 private ProducerStatusInfo producerStatusInfo(InfoProducer producer) {
338 var opState = producer.isAvailable() ? ProducerStatusInfo.OperationalState.ENABLED
339 : ProducerStatusInfo.OperationalState.DISABLED;
340 return new ProducerStatusInfo(opState);
344 path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}", //
345 produces = MediaType.APPLICATION_JSON_VALUE)
346 @Operation(summary = "Individual Information Producer", description = "")
350 responseCode = "201",
351 description = "Producer created", //
352 content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
354 responseCode = "200",
355 description = "Producer updated", //
356 content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
358 responseCode = "404",
359 description = "Producer type not found", //
360 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))), //
362 responseCode = "400",
363 description = "Input validation failed", //
364 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
366 public ResponseEntity<Object> putInfoProducer( //
367 @PathVariable("infoProducerId") String infoProducerId, //
368 @RequestBody ProducerRegistrationInfo registrationInfo) {
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());
381 private void validateUri(String url) throws ServiceException {
382 if (url != null && !url.isEmpty()) {
384 URI uri = new URI(url);
385 if (!uri.isAbsolute()) {
386 throw new ServiceException("URI: " + url + " is not absolute", HttpStatus.BAD_REQUEST);
388 } catch (URISyntaxException e) {
389 throw new ServiceException(e.getMessage(), HttpStatus.BAD_REQUEST);
392 throw new ServiceException("Missing required URL", HttpStatus.BAD_REQUEST);
397 path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}",
398 produces = MediaType.APPLICATION_JSON_VALUE)
399 @Operation(summary = "Individual Information Producer", description = "")
403 responseCode = "200",
404 description = "Not used", //
405 content = @Content(schema = @Schema(implementation = VoidResponse.class))),
407 responseCode = "204",
408 description = "Producer deleted", //
409 content = @Content(schema = @Schema(implementation = VoidResponse.class))),
411 responseCode = "404",
412 description = "Producer is not found", //
413 content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
415 public ResponseEntity<Object> deleteInfoProducer(
416 @PathVariable(ProducerConsts.INFO_PRODUCER_ID_PATH) String infoProducerId) {
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());
427 private ProducerRegistrationInfo toProducerRegistrationInfo(InfoProducer p) {
428 Collection<String> types = new ArrayList<>();
429 for (InfoType type : p.getInfoTypes()) {
430 types.add(type.getId());
432 return new ProducerRegistrationInfo(types, p.getJobCallbackUrl(), p.getProducerSupervisionCallbackUrl());
435 private ProducerInfoTypeInfo toInfoTypeInfo(InfoType t) {
436 return new ProducerInfoTypeInfo(t.getJobDataSchema(), t.getTypeSpecificInfo());
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);
447 return InfoProducerRegistrationInfo.builder() //
448 .id(infoProducerId) //
449 .jobCallbackUrl(info.jobCallbackUrl) //
450 .producerSupervisionCallbackUrl(info.producerSupervisionCallbackUrl) //
451 .supportedTypes(supportedTypes) //