X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=blobdiff_plain;f=enrichment-coordinator-service%2Fsrc%2Fmain%2Fjava%2Forg%2Foransc%2Fenrichment%2Fcontrollers%2Fr1consumer%2FConsumerController.java;h=b108380b5ba9afc002392bfdcf116df8be28f924;hb=5e1623ab25b62c6c28849bfd862eba4648465922;hp=7a1bf184a267107c894f084e9d04c749e65804dd;hpb=edea18a8fda2e2201cb3ede7f7af13f610bf4acc;p=nonrtric.git diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerController.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerController.java index 7a1bf184..b108380b 100644 --- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerController.java +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerController.java @@ -34,20 +34,24 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import java.lang.invoke.MethodHandles; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import org.json.JSONObject; -import org.oransc.enrichment.configuration.ApplicationConfig; import org.oransc.enrichment.controllers.ErrorResponse; import org.oransc.enrichment.controllers.VoidResponse; import org.oransc.enrichment.controllers.r1producer.ProducerCallbacks; import org.oransc.enrichment.exceptions.ServiceException; -import org.oransc.enrichment.repository.EiJob; -import org.oransc.enrichment.repository.EiJobs; -import org.oransc.enrichment.repository.EiProducers; -import org.oransc.enrichment.repository.EiType; -import org.oransc.enrichment.repository.EiTypes; +import org.oransc.enrichment.repository.InfoJob; +import org.oransc.enrichment.repository.InfoJobs; +import org.oransc.enrichment.repository.InfoProducer; +import org.oransc.enrichment.repository.InfoProducers; +import org.oransc.enrichment.repository.InfoType; +import org.oransc.enrichment.repository.InfoTypeSubscriptions; +import org.oransc.enrichment.repository.InfoTypes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -65,30 +69,29 @@ import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; @SuppressWarnings("java:S3457") // No need to call "toString()" method as formatting and string .. -@RestController("Consumer registry") +@RestController("Consumer API") @Tag(name = ConsumerConsts.CONSUMER_API_NAME) @RequestMapping(path = ConsumerConsts.API_ROOT, produces = MediaType.APPLICATION_JSON_VALUE) public class ConsumerController { private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - @Autowired - ApplicationConfig applicationConfig; - - @Autowired - private EiJobs jobs; - - @Autowired - private EiTypes infoTypes; - - @Autowired - private EiProducers infoProducers; - - @Autowired - ProducerCallbacks producerCallbacks; - + private final InfoJobs infoJobs; + private final InfoTypes infoTypes; + private final InfoProducers infoProducers; + private final ProducerCallbacks producerCallbacks; + private final InfoTypeSubscriptions infoTypeSubscriptions; private static Gson gson = new GsonBuilder().create(); + public ConsumerController(@Autowired InfoJobs jobs, @Autowired InfoTypes infoTypes, + @Autowired InfoProducers infoProducers, @Autowired ProducerCallbacks producerCallbacks, + @Autowired InfoTypeSubscriptions infoTypeSubscriptions) { + this.infoProducers = infoProducers; + this.infoJobs = jobs; + this.infoTypeSubscriptions = infoTypeSubscriptions; + this.infoTypes = infoTypes; + this.producerCallbacks = producerCallbacks; + } + @GetMapping(path = "/info-types", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Information type identifiers", description = "") @ApiResponses( @@ -101,7 +104,7 @@ public class ConsumerController { public ResponseEntity getinfoTypeIdentifiers( // ) { List result = new ArrayList<>(); - for (EiType infoType : this.infoTypes.getAllInfoTypes()) { + for (InfoType infoType : this.infoTypes.getAllInfoTypes()) { result.add(infoType.getId()); } @@ -124,7 +127,7 @@ public class ConsumerController { public ResponseEntity getInfoType( // @PathVariable("infoTypeId") String infoTypeId) { try { - EiType type = this.infoTypes.getType(infoTypeId); + InfoType type = this.infoTypes.getType(infoTypeId); ConsumerInfoTypeInfo info = toInfoTypeInfo(type); return new ResponseEntity<>(gson.toJson(info), HttpStatus.OK); } catch (Exception e) { @@ -133,7 +136,7 @@ public class ConsumerController { } @GetMapping(path = "/info-jobs", produces = MediaType.APPLICATION_JSON_VALUE) - @Operation(summary = "EI job identifiers", description = "query for information job identifiers") + @Operation(summary = "Information Job identifiers", description = "query for information job identifiers") @ApiResponses( value = { // @ApiResponse( @@ -159,15 +162,15 @@ public class ConsumerController { try { List result = new ArrayList<>(); if (owner != null) { - for (EiJob job : this.jobs.getJobsForOwner(owner)) { + for (InfoJob job : this.infoJobs.getJobsForOwner(owner)) { if (infoTypeId == null || job.getTypeId().equals(infoTypeId)) { result.add(job.getId()); } } } else if (infoTypeId != null) { - this.jobs.getJobsForType(infoTypeId).forEach(job -> result.add(job.getId())); + this.infoJobs.getJobsForType(infoTypeId).forEach(job -> result.add(job.getId())); } else { - this.jobs.getJobs().forEach(job -> result.add(job.getId())); + this.infoJobs.getJobs().forEach(job -> result.add(job.getId())); } return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK); } catch ( @@ -193,7 +196,7 @@ public class ConsumerController { public ResponseEntity getIndividualEiJob( // @PathVariable("infoJobId") String infoJobId) { try { - EiJob job = this.jobs.getJob(infoJobId); + InfoJob job = this.infoJobs.getJob(infoJobId); return new ResponseEntity<>(gson.toJson(toInfoJobInfo(job)), HttpStatus.OK); } catch (Exception e) { return ErrorResponse.create(e, HttpStatus.NOT_FOUND); @@ -216,17 +219,19 @@ public class ConsumerController { public ResponseEntity getEiJobStatus( // @PathVariable("infoJobId") String jobId) { try { - EiJob job = this.jobs.getJob(jobId); + InfoJob job = this.infoJobs.getJob(jobId); return new ResponseEntity<>(gson.toJson(toInfoJobStatus(job)), HttpStatus.OK); } catch (Exception e) { return ErrorResponse.create(e, HttpStatus.NOT_FOUND); } } - private ConsumerJobStatus toInfoJobStatus(EiJob job) { + private ConsumerJobStatus toInfoJobStatus(InfoJob job) { + Collection producerIds = new ArrayList<>(); + this.infoProducers.getProducersForType(job.getTypeId()).forEach(producer -> producerIds.add(producer.getId())); return this.infoProducers.isJobEnabled(job) - ? new ConsumerJobStatus(ConsumerJobStatus.InfoJobStatusValues.ENABLED) - : new ConsumerJobStatus(ConsumerJobStatus.InfoJobStatusValues.DISABLED); + ? new ConsumerJobStatus(ConsumerJobStatus.InfoJobStatusValues.ENABLED, producerIds) + : new ConsumerJobStatus(ConsumerJobStatus.InfoJobStatusValues.DISABLED, producerIds); } @@ -241,7 +246,8 @@ public class ConsumerController { @ApiResponse( responseCode = "204", description = "Job deleted", // - content = @Content(schema = @Schema(implementation = VoidResponse.class))), // "Individual EI job" + content = @Content(schema = @Schema(implementation = VoidResponse.class))), // "Individual + // Information Job" @ApiResponse( responseCode = "404", description = "Information subscription job is not found", // @@ -250,8 +256,8 @@ public class ConsumerController { public ResponseEntity deleteIndividualEiJob( // @PathVariable("infoJobId") String jobId) { try { - EiJob job = this.jobs.getJob(jobId); - this.jobs.remove(job, this.infoProducers); + InfoJob job = this.infoJobs.getJob(jobId); + this.infoJobs.remove(job, this.infoProducers); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } catch (Exception e) { return ErrorResponse.create(e, HttpStatus.NOT_FOUND); @@ -262,7 +268,7 @@ public class ConsumerController { path = "/info-jobs/{infoJobId}", // produces = MediaType.APPLICATION_JSON_VALUE, // consumes = MediaType.APPLICATION_JSON_VALUE) - @Operation(summary = ConsumerConsts.INDIVIDUAL_JOB) + @Operation(summary = ConsumerConsts.INDIVIDUAL_JOB, description = ConsumerConsts.PUT_INDIVIDUAL_JOB_DESCRIPTION) @ApiResponses( value = { // @ApiResponse( @@ -275,44 +281,197 @@ public class ConsumerController { content = @Content(schema = @Schema(implementation = VoidResponse.class))), // @ApiResponse( responseCode = "404", - description = "Enrichment Information type is not found", // - content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // - }) + description = "Information type is not found", // + content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))), // + @ApiResponse( + responseCode = "400", + description = "Input validation failed", // + content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))), // + @ApiResponse( + responseCode = "409", + description = "Cannot modify job type", // + content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class)))}) public Mono> putIndividualInfoJob( // @PathVariable("infoJobId") String jobId, // + @Parameter( + name = ConsumerConsts.PERFORM_TYPE_CHECK_PARAM, + required = false, // + description = ConsumerConsts.PERFORM_TYPE_CHECK_PARAM_DESCRIPTION) // + @RequestParam( + name = ConsumerConsts.PERFORM_TYPE_CHECK_PARAM, + required = false, + defaultValue = "false") boolean performTypeCheck, @RequestBody ConsumerJobInfo informationJobObject) { - final boolean isNewJob = this.jobs.get(jobId) == null; + final boolean isNewJob = this.infoJobs.get(jobId) == null; - return validatePutInfoJob(jobId, informationJobObject) // + return validatePutInfoJob(jobId, informationJobObject, performTypeCheck) // .flatMap(this::startInfoSubscriptionJob) // - .doOnNext(newEiJob -> this.jobs.put(newEiJob)) // - .flatMap(newEiJob -> Mono.just(new ResponseEntity<>(isNewJob ? HttpStatus.CREATED : HttpStatus.OK))) + .doOnNext(this.infoJobs::put) // + .map(newEiJob -> new ResponseEntity<>(isNewJob ? HttpStatus.CREATED : HttpStatus.OK)) // .onErrorResume(throwable -> Mono.just(ErrorResponse.create(throwable, HttpStatus.NOT_FOUND))); } - private Mono startInfoSubscriptionJob(EiJob newInfoJob) { + @GetMapping(path = "/info-type-subscription", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation( + summary = "Information type subscription identifiers", + description = "query for information type subscription identifiers") + @ApiResponses( + value = { // + @ApiResponse( + responseCode = "200", + description = "Information type subscription identifiers", // + content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))),}) + public ResponseEntity getInfoTypeSubscriptions( // + + @Parameter( + name = ConsumerConsts.OWNER_PARAM, + required = false, // + description = ConsumerConsts.OWNER_PARAM_DESCRIPTION) // + @RequestParam(name = ConsumerConsts.OWNER_PARAM, required = false) String owner) { + try { + List result = new ArrayList<>(); + if (owner != null) { + this.infoTypeSubscriptions.getSubscriptionsForOwner(owner) + .forEach(subscription -> result.add(subscription.getId())); + } else { + this.infoTypeSubscriptions.getAllSubscriptions() + .forEach(subscription -> result.add(subscription.getId())); + } + return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK); + } catch (Exception e) { + return ErrorResponse.create(e, HttpStatus.NOT_FOUND); + } + } + + @GetMapping(path = "/info-type-subscription/{subscriptionId}", produces = MediaType.APPLICATION_JSON_VALUE) // + @Operation(summary = ConsumerConsts.INDIVIDUAL_TYPE_SUBSCRIPTION, description = "") // + @ApiResponses( + value = { // + @ApiResponse( + responseCode = "200", + description = "Type subscription", // + content = @Content(schema = @Schema(implementation = ConsumerTypeSubscriptionInfo.class))), // + @ApiResponse( + responseCode = "404", + description = "Subscription is not found", // + content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // + }) + public ResponseEntity getIndividualTypeSubscription( // + @PathVariable("subscriptionId") String subscriptionId) { + try { + InfoTypeSubscriptions.SubscriptionInfo subscription = + this.infoTypeSubscriptions.getSubscription(subscriptionId); + return new ResponseEntity<>(gson.toJson(toTypeSuscriptionInfo(subscription)), HttpStatus.OK); + } catch (Exception e) { + return ErrorResponse.create(e, HttpStatus.NOT_FOUND); + } + } + + @PutMapping( + path = "/info-type-subscription/{subscriptionId}", // + produces = MediaType.APPLICATION_JSON_VALUE, // + consumes = MediaType.APPLICATION_JSON_VALUE) + @Operation( + summary = ConsumerConsts.INDIVIDUAL_TYPE_SUBSCRIPTION, + description = ConsumerConsts.TYPE_SUBSCRIPTION_DESCRIPTION) + @ApiResponses( + value = { // + @ApiResponse( + responseCode = "201", + description = "Subscription created", // + content = @Content(schema = @Schema(implementation = VoidResponse.class))), // + @ApiResponse( + responseCode = "200", + description = "Subscription updated", // + content = @Content(schema = @Schema(implementation = VoidResponse.class))) // + }) + public Mono> putIndividualTypeSubscription( // + @PathVariable("subscriptionId") String subscriptionId, // + @RequestBody ConsumerTypeSubscriptionInfo subscription) { + + final boolean isNewSubscription = this.infoTypeSubscriptions.get(subscriptionId) == null; + this.infoTypeSubscriptions.put(toTypeSuscriptionInfo(subscription, subscriptionId)); + return Mono.just(new ResponseEntity<>(isNewSubscription ? HttpStatus.CREATED : HttpStatus.OK)); + } + + @DeleteMapping(path = "/info-type-subscription/{subscriptionId}", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = ConsumerConsts.INDIVIDUAL_TYPE_SUBSCRIPTION, description = "") + @ApiResponses( + value = { // + @ApiResponse( + responseCode = "200", + description = "Not used", // + content = @Content(schema = @Schema(implementation = VoidResponse.class))), + @ApiResponse( + responseCode = "204", + description = "Subscription deleted", // + content = @Content(schema = @Schema(implementation = VoidResponse.class))), + @ApiResponse( + responseCode = "404", + description = "Subscription is not found", // + content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // + }) + public ResponseEntity deleteIndividualTypeSubscription( // + @PathVariable("subscriptionId") String subscriptionId) { + try { + InfoTypeSubscriptions.SubscriptionInfo subscription = + this.infoTypeSubscriptions.getSubscription(subscriptionId); + this.infoTypeSubscriptions.remove(subscription); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } catch (Exception e) { + return ErrorResponse.create(e, HttpStatus.NOT_FOUND); + } + } + + private ConsumerTypeSubscriptionInfo toTypeSuscriptionInfo(InfoTypeSubscriptions.SubscriptionInfo s) { + return new ConsumerTypeSubscriptionInfo(s.getCallbackUrl(), s.getOwner()); + } + + private InfoTypeSubscriptions.SubscriptionInfo toTypeSuscriptionInfo(ConsumerTypeSubscriptionInfo s, + String subscriptionId) { + return InfoTypeSubscriptions.SubscriptionInfo.builder() // + .apiVersion(ConsumerCallbacks.API_VERSION) // + .owner(s.owner) // + .id(subscriptionId) // + .callbackUrl(s.statusResultUri).build(); + } + + private Mono startInfoSubscriptionJob(InfoJob newInfoJob) { return this.producerCallbacks.startInfoSubscriptionJob(newInfoJob, infoProducers) // .doOnNext(noOfAcceptingProducers -> this.logger.debug("Started job {}, number of activated producers: {}", newInfoJob.getId(), noOfAcceptingProducers)) // - .flatMap(noOfAcceptingProducers -> Mono.just(newInfoJob)); + .map(noOfAcceptingProducers -> newInfoJob); } - private Mono validatePutInfoJob(String jobId, ConsumerJobInfo jobInfo) { + private Mono validatePutInfoJob(String jobId, ConsumerJobInfo jobInfo, boolean performTypeCheck) { try { - EiType infoType = this.infoTypes.getType(jobInfo.infoTypeId); - validateJsonObjectAgainstSchema(infoType.getJobDataSchema(), jobInfo.jobDefinition); - EiJob existingEiJob = this.jobs.get(jobId); + if (performTypeCheck) { + InfoType infoType = this.infoTypes.getType(jobInfo.infoTypeId); + validateJsonObjectAgainstSchema(infoType.getJobDataSchema(), jobInfo.jobDefinition); + } + InfoJob existingEiJob = this.infoJobs.get(jobId); + validateUri(jobInfo.statusNotificationUri); + validateUri(jobInfo.jobResultUri); if (existingEiJob != null && !existingEiJob.getTypeId().equals(jobInfo.infoTypeId)) { throw new ServiceException("Not allowed to change type for existing job", HttpStatus.CONFLICT); } - return Mono.just(toEiJob(jobInfo, jobId, infoType)); + return Mono.just(toEiJob(jobInfo, jobId, jobInfo.infoTypeId)); } catch (Exception e) { return Mono.error(e); } } + private void validateUri(String url) throws URISyntaxException, ServiceException { + if (url != null && !url.isEmpty()) { + URI uri = new URI(url); + if (!uri.isAbsolute()) { + throw new ServiceException("URI: " + url + " is not absolute", HttpStatus.BAD_REQUEST); + } + } + } + private void validateJsonObjectAgainstSchema(Object schemaObj, Object object) throws ServiceException { if (schemaObj != null) { // schema is optional for now try { @@ -320,21 +479,21 @@ public class ConsumerController { String schemaAsString = mapper.writeValueAsString(schemaObj); JSONObject schemaJSON = new JSONObject(schemaAsString); - org.everit.json.schema.Schema schema = org.everit.json.schema.loader.SchemaLoader.load(schemaJSON); + var schema = org.everit.json.schema.loader.SchemaLoader.load(schemaJSON); String objectAsString = mapper.writeValueAsString(object); JSONObject json = new JSONObject(objectAsString); schema.validate(json); } catch (Exception e) { - throw new ServiceException("Json validation failure " + e.toString(), HttpStatus.CONFLICT); + throw new ServiceException("Json validation failure " + e.toString(), HttpStatus.BAD_REQUEST); } } } - private EiJob toEiJob(ConsumerJobInfo info, String id, EiType type) { - return EiJob.builder() // + private InfoJob toEiJob(ConsumerJobInfo info, String id, String typeId) { + return InfoJob.builder() // .id(id) // - .typeId(type.getId()) // + .typeId(typeId) // .owner(info.owner) // .jobData(info.jobDefinition) // .targetUrl(info.jobResultUri) // @@ -342,11 +501,21 @@ public class ConsumerController { .build(); } - private ConsumerInfoTypeInfo toInfoTypeInfo(EiType type) { - return new ConsumerInfoTypeInfo(type.getJobDataSchema()); + private ConsumerInfoTypeInfo toInfoTypeInfo(InfoType type) { + return new ConsumerInfoTypeInfo(type.getJobDataSchema(), typeStatus(type), + this.infoProducers.getProducerIdsForType(type.getId()).size()); + } + + private ConsumerInfoTypeInfo.ConsumerTypeStatusValues typeStatus(InfoType type) { + for (InfoProducer producer : this.infoProducers.getProducersForType(type)) { + if (producer.isAvailable()) { + return ConsumerInfoTypeInfo.ConsumerTypeStatusValues.ENABLED; + } + } + return ConsumerInfoTypeInfo.ConsumerTypeStatusValues.DISABLED; } - private ConsumerJobInfo toInfoJobInfo(EiJob s) { + private ConsumerJobInfo toInfoJobInfo(InfoJob s) { return new ConsumerJobInfo(s.getTypeId(), s.getJobData(), s.getOwner(), s.getTargetUrl(), s.getJobStatusUrl()); } }