From 8a8fc3daff39f7fad8b1bcdfa6e1a954a02dea48 Mon Sep 17 00:00:00 2001 From: PatrikBuhr Date: Mon, 10 Oct 2022 15:08:20 +0200 Subject: [PATCH] NONRTRIC - A scheme for versioning of types. If a consumer creates a job and the requested type does not exist, a compatible type may be automatically chosen. The type with the lowest version number is chosen. All producers that has been registerred to support a compatible type are notified. The producers will one of its registerred types (the one with the lowest number). Change-Id: I94580f3fd1e07c5b0483232e06d1950c2c573fea Signed-off-by: PatrikBuhr Issue-ID: NONRTRIC-810 --- api/ics-api.json | 18 ++- api/ics-api.yaml | 22 ++- src/main/java/org/oransc/ics/BeanFactory.java | 4 +- .../org/oransc/ics/controllers/a1e/A1eConsts.java | 4 + .../oransc/ics/controllers/a1e/A1eController.java | 58 +++---- .../ics/controllers/r1consumer/ConsumerConsts.java | 4 +- .../controllers/r1consumer/ConsumerController.java | 61 ++++---- .../controllers/r1producer/ProducerCallbacks.java | 11 +- .../ics/controllers/r1producer/ProducerConsts.java | 2 + .../controllers/r1producer/ProducerController.java | 36 +++-- .../controllers/r1producer/ProducerJobInfo.java | 3 +- .../java/org/oransc/ics/repository/InfoJob.java | 41 ++++- .../java/org/oransc/ics/repository/InfoJobs.java | 41 +++-- .../org/oransc/ics/repository/InfoProducer.java | 18 ++- .../repository/InfoProducerRegistrationInfo.java | 38 +++++ .../org/oransc/ics/repository/InfoProducers.java | 81 ++++------ .../java/org/oransc/ics/repository/InfoType.java | 163 ++++++++++++++++++++ .../ics/repository/InfoTypeSubscriptions.java | 26 +++- .../java/org/oransc/ics/repository/InfoTypes.java | 37 +++-- .../java/org/oransc/ics/repository/MultiMap.java | 33 ++-- src/test/java/org/oransc/ics/ApplicationTest.java | 167 ++++++++++++++++----- .../controller/ProducerSimulatorController.java | 6 +- 22 files changed, 644 insertions(+), 230 deletions(-) create mode 100644 src/main/java/org/oransc/ics/repository/InfoProducerRegistrationInfo.java diff --git a/api/ics-api.json b/api/ics-api.json index 3dece23..d3b72cc 100644 --- a/api/ics-api.json +++ b/api/ics-api.json @@ -393,6 +393,7 @@ }, "delete": { "summary": "Individual Information Type", + "description": "Existing jobs of the type will be automatically deleted.", "operationId": "deleteInfoType", "responses": { "200": { @@ -726,7 +727,7 @@ }}, "/A1-EI/v1/eijobs/{eiJobId}/status": {"get": { "summary": "EI job status", - "operationId": "getEiJobStatus_1", + "operationId": "getEiJobStatus", "responses": { "200": { "description": "EI job status", @@ -768,7 +769,7 @@ }}, "/data-consumer/v1/info-jobs/{infoJobId}/status": {"get": { "summary": "Job status", - "operationId": "getEiJobStatus", + "operationId": "getInfoJobStatus", "responses": { "200": { "description": "Information subscription job status", @@ -950,7 +951,7 @@ "/A1-EI/v1/eijobs/{eiJobId}": { "get": { "summary": "Individual EI job", - "operationId": "getIndividualEiJob_1", + "operationId": "getIndividualEiJob", "responses": { "200": { "description": "EI job", @@ -971,7 +972,7 @@ }, "delete": { "summary": "Individual EI job", - "operationId": "deleteIndividualEiJob_1", + "operationId": "deleteIndividualEiJob", "responses": { "200": { "description": "Not used", @@ -1000,6 +1001,7 @@ "content": {"application/json": {"schema": {"$ref": "#/components/schemas/EiJobObject"}}}, "required": true }, + "description": "If the requested info_type_id is not found, an attempt to find a compatible version is made. As an example, 'type_1.9.0' is backwards compatible with 'type_1.0.0'", "operationId": "putIndividualEiJob", "responses": { "200": { @@ -1044,7 +1046,7 @@ "/data-consumer/v1/info-jobs/{infoJobId}": { "get": { "summary": "Individual data subscription job", - "operationId": "getIndividualEiJob", + "operationId": "getIndividualInfoJob", "responses": { "200": { "description": "Information subscription job", @@ -1065,7 +1067,7 @@ }, "delete": { "summary": "Individual data subscription job", - "operationId": "deleteIndividualEiJob", + "operationId": "deleteIndividualInfoJob", "responses": { "200": { "description": "Not used", @@ -1094,7 +1096,7 @@ "content": {"application/json": {"schema": {"$ref": "#/components/schemas/consumer_job"}}}, "required": true }, - "description": "The job will be enabled when a producer is available", + "description": "The job will be enabled when a producer is available. If the requested info_type_id is not found, an attempt to find a compatible version is made. As an example, 'type_1.9.0' is backwards compatible with 'type_1.0.0'", "operationId": "putIndividualInfoJob", "responses": { "200": { @@ -1141,7 +1143,7 @@ "schema": {"type": "string"}, "in": "query", "name": "infoTypeId", - "description": "If given, only the producers for the EI Data type is returned.", + "description": "If given, only the producers for the Info Type is returned.", "required": false }], "tags": ["Data producer (registration)"] diff --git a/api/ics-api.yaml b/api/ics-api.yaml index 653f529..ab9ca1f 100644 --- a/api/ics-api.yaml +++ b/api/ics-api.yaml @@ -174,6 +174,7 @@ paths: tags: - Data producer (registration) summary: Individual Information Type + description: Existing jobs of the type will be automatically deleted. operationId: deleteInfoType parameters: - name: infoTypeId @@ -568,7 +569,7 @@ paths: tags: - A1-EI (registration) summary: EI job status - operationId: getEiJobStatus_1 + operationId: getEiJobStatus parameters: - name: eiJobId in: path @@ -622,7 +623,7 @@ paths: tags: - Data consumer summary: Job status - operationId: getEiJobStatus + operationId: getInfoJobStatus parameters: - name: infoJobId in: path @@ -860,7 +861,7 @@ paths: tags: - A1-EI (registration) summary: Individual EI job - operationId: getIndividualEiJob_1 + operationId: getIndividualEiJob parameters: - name: eiJobId in: path @@ -886,6 +887,9 @@ paths: tags: - A1-EI (registration) summary: Individual EI job + description: If the requested info_type_id is not found, an attempt to find + a compatible version is made. As an example, 'type_1.9.0' is backwards compatible + with 'type_1.0.0' operationId: putIndividualEiJob parameters: - name: eiJobId @@ -936,7 +940,7 @@ paths: tags: - A1-EI (registration) summary: Individual EI job - operationId: deleteIndividualEiJob_1 + operationId: deleteIndividualEiJob parameters: - name: eiJobId in: path @@ -982,7 +986,7 @@ paths: tags: - Data consumer summary: Individual data subscription job - operationId: getIndividualEiJob + operationId: getIndividualInfoJob parameters: - name: infoJobId in: path @@ -1008,7 +1012,9 @@ paths: tags: - Data consumer summary: Individual data subscription job - description: The job will be enabled when a producer is available + description: The job will be enabled when a producer is available. If the requested + info_type_id is not found, an attempt to find a compatible version is made. + As an example, 'type_1.9.0' is backwards compatible with 'type_1.0.0' operationId: putIndividualInfoJob parameters: - name: infoJobId @@ -1059,7 +1065,7 @@ paths: tags: - Data consumer summary: Individual data subscription job - operationId: deleteIndividualEiJob + operationId: deleteIndividualInfoJob parameters: - name: infoJobId in: path @@ -1096,7 +1102,7 @@ paths: parameters: - name: infoTypeId in: query - description: If given, only the producers for the EI Data type is returned. + description: If given, only the producers for the Info Type is returned. required: false style: form explode: true diff --git a/src/main/java/org/oransc/ics/BeanFactory.java b/src/main/java/org/oransc/ics/BeanFactory.java index 3847cc3..ac6190f 100644 --- a/src/main/java/org/oransc/ics/BeanFactory.java +++ b/src/main/java/org/oransc/ics/BeanFactory.java @@ -66,9 +66,9 @@ class BeanFactory { } @Bean - public InfoJobs infoJobs(SecurityContext securityContext) { + public InfoJobs infoJobs(SecurityContext securityContext, InfoTypes types) { if (infoJobs == null) { - infoJobs = new InfoJobs(getApplicationConfig(), producerCallbacks(securityContext)); + infoJobs = new InfoJobs(getApplicationConfig(), types, producerCallbacks(securityContext)); try { infoJobs.restoreJobsFromDatabase(); } catch (Exception e) { diff --git a/src/main/java/org/oransc/ics/controllers/a1e/A1eConsts.java b/src/main/java/org/oransc/ics/controllers/a1e/A1eConsts.java index 03d8ce2..5ee7715 100644 --- a/src/main/java/org/oransc/ics/controllers/a1e/A1eConsts.java +++ b/src/main/java/org/oransc/ics/controllers/a1e/A1eConsts.java @@ -35,6 +35,10 @@ public class A1eConsts { public static final String EI_TYPE_ID_PARAM = "eiTypeId"; public static final String EI_TYPE_ID_PARAM_DESCRIPTION = "selects EI jobs of matching EI type"; + public static final String PUT_INDIVIDUAL_JOB_DESCRIPTION = + "If the requested info_type_id is not found, an attempt to find a compatible version is made. " // + + "As an example, 'type_1.9.0' is backwards compatible with 'type_1.0.0'"; + private A1eConsts() { } } diff --git a/src/main/java/org/oransc/ics/controllers/a1e/A1eController.java b/src/main/java/org/oransc/ics/controllers/a1e/A1eController.java index 0c2aca6..9665576 100644 --- a/src/main/java/org/oransc/ics/controllers/a1e/A1eController.java +++ b/src/main/java/org/oransc/ics/controllers/a1e/A1eController.java @@ -78,10 +78,10 @@ public class A1eController { ApplicationConfig applicationConfig; @Autowired - private InfoJobs eiJobs; + private InfoJobs infoJobs; @Autowired - private InfoTypes eiTypes; + private InfoTypes infoTypes; @Autowired private InfoProducers infoProducers; @@ -103,7 +103,7 @@ public class A1eController { public ResponseEntity getEiTypeIdentifiers( // ) { List result = new ArrayList<>(); - for (InfoType eiType : this.eiTypes.getAllInfoTypes()) { + for (InfoType eiType : this.infoTypes.getAllInfoTypes()) { result.add(eiType.getId()); } @@ -126,7 +126,7 @@ public class A1eController { public ResponseEntity getEiType( // @PathVariable("eiTypeId") String eiTypeId) { try { - this.eiTypes.getType(eiTypeId); // Make sure that the type exists + this.infoTypes.getType(eiTypeId); // Make sure that the type exists A1eEiTypeInfo info = toEiTypeInfo(); return new ResponseEntity<>(gson.toJson(info), HttpStatus.OK); } catch (Exception e) { @@ -161,15 +161,15 @@ public class A1eController { try { List result = new ArrayList<>(); if (owner != null) { - for (InfoJob job : this.eiJobs.getJobsForOwner(owner)) { - if (eiTypeId == null || job.getTypeId().equals(eiTypeId)) { + for (InfoJob job : this.infoJobs.getJobsForOwner(owner)) { + if (eiTypeId == null || job.getType().getId().equals(eiTypeId)) { result.add(job.getId()); } } } else if (eiTypeId != null) { - this.eiJobs.getJobsForType(eiTypeId).forEach(job -> result.add(job.getId())); + this.infoJobs.getJobsForType(eiTypeId).forEach(job -> result.add(job.getId())); } else { - this.eiJobs.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 ( @@ -195,7 +195,7 @@ public class A1eController { public ResponseEntity getIndividualEiJob( // @PathVariable("eiJobId") String eiJobId) { try { - InfoJob job = this.eiJobs.getJob(eiJobId); + InfoJob job = this.infoJobs.getJob(eiJobId); return new ResponseEntity<>(gson.toJson(toEiJobInfo(job)), HttpStatus.OK); } catch (Exception e) { return ErrorResponse.create(e, HttpStatus.NOT_FOUND); @@ -218,7 +218,7 @@ public class A1eController { public ResponseEntity getEiJobStatus( // @PathVariable("eiJobId") String eiJobId) { try { - InfoJob job = this.eiJobs.getJob(eiJobId); + InfoJob job = this.infoJobs.getJob(eiJobId); return new ResponseEntity<>(gson.toJson(toEiJobStatus(job)), HttpStatus.OK); } catch (Exception e) { return ErrorResponse.create(e, HttpStatus.NOT_FOUND); @@ -251,8 +251,8 @@ public class A1eController { public ResponseEntity deleteIndividualEiJob( // @PathVariable("eiJobId") String eiJobId) { try { - InfoJob job = this.eiJobs.getJob(eiJobId); - this.eiJobs.remove(job, this.infoProducers); + InfoJob job = this.infoJobs.getJob(eiJobId); + this.infoJobs.remove(job, this.infoProducers); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } catch (Exception e) { return ErrorResponse.create(e, HttpStatus.NOT_FOUND); @@ -263,7 +263,7 @@ public class A1eController { path = "/eijobs/{eiJobId}", // produces = MediaType.APPLICATION_JSON_VALUE, // consumes = MediaType.APPLICATION_JSON_VALUE) - @Operation(summary = "Individual EI job", description = "") + @Operation(summary = "Individual EI job", description = A1eConsts.PUT_INDIVIDUAL_JOB_DESCRIPTION) @ApiResponses( value = { // @ApiResponse( @@ -291,34 +291,37 @@ public class A1eController { public Mono> putIndividualEiJob( // @PathVariable("eiJobId") String eiJobId, // - @RequestBody A1eEiJobInfo eiJobObject) { + @RequestBody A1eEiJobInfo eiJobObject) throws ServiceException { - final boolean isNewJob = this.eiJobs.get(eiJobId) == null; + final boolean isNewJob = this.infoJobs.get(eiJobId) == null; + InfoType eiType = this.infoTypes.getCompatibleType(eiJobObject.eiTypeId); - return validatePutEiJob(eiJobId, eiJobObject) // - .flatMap(this::startEiJob) // - .doOnNext(newEiJob -> this.eiJobs.put(newEiJob)) // + return validatePutEiJob(eiJobId, eiType, eiJobObject) // + .flatMap(job -> startEiJob(job, eiType)) // + .doOnNext(newEiJob -> this.infoJobs.put(newEiJob)) // .map(newEiJob -> new ResponseEntity<>(isNewJob ? HttpStatus.CREATED : HttpStatus.OK)) // .onErrorResume(throwable -> Mono.just(ErrorResponse.create(throwable, HttpStatus.INTERNAL_SERVER_ERROR))); } - private Mono startEiJob(InfoJob newEiJob) { - return this.producerCallbacks.startInfoSubscriptionJob(newEiJob, infoProducers) // + private Mono startEiJob(InfoJob newEiJob, InfoType type) { + return this.producerCallbacks.startInfoSubscriptionJob(newEiJob, type, infoProducers) // .doOnNext(noOfAcceptingProducers -> this.logger.debug( "Started EI job {}, number of activated producers: {}", newEiJob.getId(), noOfAcceptingProducers)) // .map(noOfAcceptingProducers -> newEiJob); } - private Mono validatePutEiJob(String eiJobId, A1eEiJobInfo eiJobInfo) { + private Mono validatePutEiJob(String eiJobId, InfoType eiType, A1eEiJobInfo eiJobInfo) { try { - InfoType eiType = this.eiTypes.getType(eiJobInfo.eiTypeId); validateJsonObjectAgainstSchema(eiType.getJobDataSchema(), eiJobInfo.jobDefinition); - InfoJob existingEiJob = this.eiJobs.get(eiJobId); validateUri(eiJobInfo.jobResultUri); validateUri(eiJobInfo.statusNotificationUri); - if (existingEiJob != null && !existingEiJob.getTypeId().equals(eiJobInfo.eiTypeId)) { - throw new ServiceException("Not allowed to change type for existing EI job", HttpStatus.CONFLICT); + InfoJob existingEiJob = this.infoJobs.get(eiJobId); + if (existingEiJob != null) { + InfoType.TypeId typeId = InfoType.TypeId.ofString(eiJobInfo.eiTypeId); + if (!existingEiJob.getType().getId().contains(typeId.getName())) { + throw new ServiceException("Not allowed to change type for existing EI job", HttpStatus.CONFLICT); + } } return Mono.just(toEiJob(eiJobInfo, eiJobId, eiType)); } catch (Exception e) { @@ -354,7 +357,7 @@ public class A1eController { private InfoJob toEiJob(A1eEiJobInfo info, String id, InfoType type) { return InfoJob.builder() // .id(id) // - .typeId(type.getId()) // + .type(type) // .owner(info.owner) // .jobData(info.jobDefinition) // .targetUrl(info.jobResultUri) // @@ -367,6 +370,7 @@ public class A1eController { } private A1eEiJobInfo toEiJobInfo(InfoJob s) { - return new A1eEiJobInfo(s.getTypeId(), s.getJobData(), s.getOwner(), s.getTargetUrl(), s.getJobStatusUrl()); + return new A1eEiJobInfo(s.getType().getId(), s.getJobData(), s.getOwner(), s.getTargetUrl(), + s.getJobStatusUrl()); } } diff --git a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerConsts.java b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerConsts.java index 00ab0f2..f53c0c5 100644 --- a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerConsts.java +++ b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerConsts.java @@ -33,7 +33,9 @@ public class ConsumerConsts { public static final String INDIVIDUAL_JOB = "Individual data subscription job"; - public static final String PUT_INDIVIDUAL_JOB_DESCRIPTION = "The job will be enabled when a producer is available"; + public static final String PUT_INDIVIDUAL_JOB_DESCRIPTION = "The job will be enabled when a producer is available. " + + "If the requested info_type_id is not found, an attempt to find a compatible version is made. " // + + "As an example, 'type_1.9.0' is backwards compatible with 'type_1.0.0'"; public static final String INFO_TYPE_ID_PARAM = "infoTypeId"; public static final String INFO_TYPE_ID_PARAM_DESCRIPTION = diff --git a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerController.java b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerController.java index e8b6dff..7de9813 100644 --- a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerController.java +++ b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerController.java @@ -166,7 +166,7 @@ public class ConsumerController { List result = new ArrayList<>(); if (owner != null) { for (InfoJob job : this.infoJobs.getJobsForOwner(owner)) { - if (infoTypeId == null || job.getTypeId().equals(infoTypeId)) { + if (infoTypeId == null || job.getType().getId().equals(infoTypeId)) { result.add(job.getId()); } } @@ -176,9 +176,7 @@ public class ConsumerController { this.infoJobs.getJobs().forEach(job -> result.add(job.getId())); } return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK); - } catch ( - - Exception e) { + } catch (Exception e) { return ErrorResponse.create(e, HttpStatus.NOT_FOUND); } } @@ -198,8 +196,7 @@ public class ConsumerController { @RequestParam(name = ConsumerConsts.OWNER_PARAM, required = true) String owner) { for (InfoJob job : this.infoJobs.getJobsForOwner(owner)) { - - logger.debug("DELETE info jobs, id: {}, type: {}, owner: {}", job.getId(), job.getTypeId(), owner); + logger.debug("DELETE info jobs, id: {}, type: {}, owner: {}", job.getId(), job.getType().getId(), owner); this.infoJobs.remove(job, this.infoProducers); } return new ResponseEntity<>(HttpStatus.NO_CONTENT); @@ -218,7 +215,7 @@ public class ConsumerController { description = "Information subscription job is not found", // content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // }) - public ResponseEntity getIndividualEiJob( // + public ResponseEntity getIndividualInfoJob( // @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String infoJobId) { try { logger.debug("GET info job, id: {}", infoJobId); @@ -242,7 +239,7 @@ public class ConsumerController { description = "Information subscription job is not found", // content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // }) - public ResponseEntity getEiJobStatus( // + public ResponseEntity getInfoJobStatus( // @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String jobId) { try { logger.debug("GET info job status, id: {}", jobId); @@ -255,7 +252,8 @@ public class ConsumerController { private ConsumerJobStatus toInfoJobStatus(InfoJob job) { Collection producerIds = new ArrayList<>(); - this.infoProducers.getProducersForType(job.getTypeId()).forEach(producer -> producerIds.add(producer.getId())); + this.infoProducers.getProducersSupportingType(job.getType()) + .forEach(producer -> producerIds.add(producer.getId())); return this.infoProducers.isJobEnabled(job) ? new ConsumerJobStatus(ConsumerJobStatus.InfoJobStatusValues.ENABLED, producerIds) : new ConsumerJobStatus(ConsumerJobStatus.InfoJobStatusValues.DISABLED, producerIds); @@ -280,7 +278,7 @@ public class ConsumerController { description = "Information subscription job is not found", // content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) // }) - public ResponseEntity deleteIndividualEiJob( // + public ResponseEntity deleteIndividualInfoJob( // @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String jobId) { try { logger.debug("DELETE info job, id: {}", jobId); @@ -321,16 +319,17 @@ public class ConsumerController { content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class)))}) public Mono> putIndividualInfoJob( // @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String jobId, // - @RequestBody ConsumerJobInfo informationJobObject) { + @RequestBody ConsumerJobInfo informationJobObject) throws ServiceException { final boolean isNewJob = this.infoJobs.get(jobId) == null; logger.debug("PUT info job, id: {}, obj: {}", jobId, informationJobObject); + InfoType infoType = this.infoTypes.getCompatibleType(informationJobObject.infoTypeId); - return validatePutInfoJob(jobId, informationJobObject) // - .flatMap(this::startInfoSubscriptionJob) // + return validatePutInfoJob(jobId, infoType, informationJobObject) // + .flatMap(job -> startInfoSubscriptionJob(job, infoType)) // .doOnNext(this.infoJobs::put) // - .map(newEiJob -> new ResponseEntity<>(isNewJob ? HttpStatus.CREATED : HttpStatus.OK)) // + .map(newJob -> new ResponseEntity<>(isNewJob ? HttpStatus.CREATED : HttpStatus.OK)) // .onErrorResume(throwable -> Mono.just(ErrorResponse.create(throwable, HttpStatus.NOT_FOUND))); } @@ -463,27 +462,28 @@ public class ConsumerController { .callbackUrl(s.statusResultUri).build(); } - private Mono startInfoSubscriptionJob(InfoJob newInfoJob) { - return this.producerCallbacks.startInfoSubscriptionJob(newInfoJob, infoProducers) // + private Mono startInfoSubscriptionJob(InfoJob newInfoJob, InfoType type) { + return this.producerCallbacks.startInfoSubscriptionJob(newInfoJob, type, infoProducers) // .doOnNext(noOfAcceptingProducers -> this.logger.debug("Started job {}, number of activated producers: {}", newInfoJob.getId(), noOfAcceptingProducers)) // .map(noOfAcceptingProducers -> newInfoJob); } - private Mono validatePutInfoJob(String jobId, ConsumerJobInfo jobInfo) { + private Mono validatePutInfoJob(String jobId, InfoType infoType, ConsumerJobInfo jobInfo) { try { - - 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); + InfoJob existingJob = this.infoJobs.get(jobId); + if (existingJob != null) { + InfoType.TypeId typeId = InfoType.TypeId.ofString(jobInfo.infoTypeId); + if (!existingJob.getType().getId().contains(typeId.getName())) { + throw new ServiceException("Not allowed to change type for existing job", HttpStatus.CONFLICT); + } } - return Mono.just(toEiJob(jobInfo, jobId, jobInfo.infoTypeId)); + + return Mono.just(toInfoJob(jobInfo, jobId, infoType)); } catch (Exception e) { return Mono.error(e); } @@ -514,10 +514,10 @@ public class ConsumerController { } } - private InfoJob toEiJob(ConsumerJobInfo info, String id, String typeId) { + private InfoJob toInfoJob(ConsumerJobInfo info, String id, InfoType type) { return InfoJob.builder() // .id(id) // - .typeId(typeId) // + .type(type) // .owner(info.owner) // .jobData(info.jobDefinition) // .targetUrl(info.jobResultUri) // @@ -527,11 +527,11 @@ public class ConsumerController { private ConsumerInfoTypeInfo toInfoTypeInfo(InfoType type) { return new ConsumerInfoTypeInfo(type.getJobDataSchema(), typeStatus(type), - this.infoProducers.getProducerIdsForType(type.getId()).size()); + this.infoProducers.getProducerIdsForType(type).size()); } private ConsumerInfoTypeInfo.ConsumerTypeStatusValues typeStatus(InfoType type) { - for (InfoProducer producer : this.infoProducers.getProducersForType(type)) { + for (InfoProducer producer : this.infoProducers.getProducersSupportingType(type)) { if (producer.isAvailable()) { return ConsumerInfoTypeInfo.ConsumerTypeStatusValues.ENABLED; } @@ -539,7 +539,8 @@ public class ConsumerController { return ConsumerInfoTypeInfo.ConsumerTypeStatusValues.DISABLED; } - private ConsumerJobInfo toInfoJobInfo(InfoJob s) { - return new ConsumerJobInfo(s.getTypeId(), s.getJobData(), s.getOwner(), s.getTargetUrl(), s.getJobStatusUrl()); + private ConsumerJobInfo toInfoJobInfo(InfoJob j) { + return new ConsumerJobInfo(j.getType().getId(), j.getJobData(), j.getOwner(), j.getTargetUrl(), + j.getJobStatusUrl()); } } diff --git a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerCallbacks.java b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerCallbacks.java index 0881cd7..2cb598e 100644 --- a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerCallbacks.java +++ b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerCallbacks.java @@ -35,6 +35,7 @@ import org.oransc.ics.repository.InfoJob; import org.oransc.ics.repository.InfoJobs; import org.oransc.ics.repository.InfoProducer; import org.oransc.ics.repository.InfoProducers; +import org.oransc.ics.repository.InfoType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,7 +65,7 @@ public class ProducerCallbacks { } public void stopInfoJob(InfoJob infoJob, InfoProducers infoProducers) { - for (InfoProducer producer : getProducersForJob(infoJob, infoProducers)) { + for (InfoProducer producer : getProducersForJob(infoJob.getType(), infoProducers)) { String url = producer.getJobCallbackUrl() + "/" + infoJob.getId(); producer.setJobDisabled(infoJob); restClient.delete(url) // @@ -81,9 +82,9 @@ public class ProducerCallbacks { * @param infoJob an Information Job * @return the number of producers that returned OK */ - public Mono startInfoSubscriptionJob(InfoJob infoJob, InfoProducers infoProducers) { + public Mono startInfoSubscriptionJob(InfoJob infoJob, InfoType type, InfoProducers infoProducers) { Retry retrySpec = Retry.fixedDelay(1, Duration.ofSeconds(1)); - return Flux.fromIterable(getProducersForJob(infoJob, infoProducers)) // + return Flux.fromIterable(getProducersForJob(type, infoProducers)) // .flatMap(infoProducer -> startInfoJob(infoProducer, infoJob, retrySpec)) // .collectList() // .map(okResponses -> Integer.valueOf(okResponses.size())); // @@ -120,8 +121,8 @@ public class ProducerCallbacks { .doOnNext(resp -> producer.setJobEnabled(infoJob)); } - private Collection getProducersForJob(InfoJob infoJob, InfoProducers infoProducers) { - return infoProducers.getProducersForType(infoJob.getTypeId()); + private Collection getProducersForJob(InfoType type, InfoProducers infoProducers) { + return infoProducers.getProducersSupportingType(type); } } diff --git a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerConsts.java b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerConsts.java index e3f667a..bc94c1e 100644 --- a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerConsts.java +++ b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerConsts.java @@ -35,6 +35,8 @@ public class ProducerConsts { public static final String INFO_PRODUCER_ID_PATH = "infoProducerId"; + public static final String DELETE_INFO_TYPE_DESCRPTION = "Existing jobs of the type will be automatically deleted."; + private ProducerConsts() { } diff --git a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerController.java b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerController.java index 5241001..15ea11a 100644 --- a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerController.java +++ b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerController.java @@ -37,7 +37,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.oransc.ics.controllers.ErrorResponse; import org.oransc.ics.controllers.VoidResponse; @@ -45,6 +47,7 @@ import org.oransc.ics.exceptions.ServiceException; import org.oransc.ics.repository.InfoJob; import org.oransc.ics.repository.InfoJobs; import org.oransc.ics.repository.InfoProducer; +import org.oransc.ics.repository.InfoProducerRegistrationInfo; import org.oransc.ics.repository.InfoProducers; import org.oransc.ics.repository.InfoType; import org.oransc.ics.repository.InfoTypeSubscriptions; @@ -167,7 +170,7 @@ public class ProducerController { @DeleteMapping( path = ProducerConsts.API_ROOT + "/info-types/{infoTypeId}", produces = MediaType.APPLICATION_JSON_VALUE) // - @Operation(summary = "Individual Information Type", description = "") // + @Operation(summary = "Individual Information Type", description = ProducerConsts.DELETE_INFO_TYPE_DESCRPTION) // @ApiResponses( value = { // @ApiResponse( @@ -195,12 +198,13 @@ public class ProducerController { if (type == null) { return ErrorResponse.create("Information type not found", HttpStatus.NOT_FOUND); } - if (!this.infoProducers.getProducersForType(type).isEmpty()) { - String firstProducerId = this.infoProducers.getProducersForType(type).iterator().next().getId(); + if (!this.infoProducers.getProducersSupportingType(type).isEmpty()) { + String firstProducerId = this.infoProducers.getProducersSupportingType(type).iterator().next().getId(); return ErrorResponse.create("The type has active producers: " + firstProducerId, HttpStatus.CONFLICT); } this.infoTypes.remove(type); - infoJobs.getJobsForType(type).forEach(job -> infoJobs.remove(job, infoProducers)); // Delete jobs for the type + infoJobs.getJobsForType(type).forEach(job -> infoJobs.remove(job, infoProducers)); // Delete jobs for the + // type this.typeSubscriptions.notifyTypeRemoved(type); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @@ -218,19 +222,29 @@ public class ProducerController { @Parameter( name = ProducerConsts.INFO_TYPE_ID_PARAM, required = false, - description = "If given, only the producers for the EI Data type is returned.") // + description = "If given, only the producers for the Info Type is returned.") // @RequestParam(name = ProducerConsts.INFO_TYPE_ID_PARAM, required = false) String typeId // - ) { + ) throws ServiceException { logger.debug("GET producer identifiers"); List result = new ArrayList<>(); - for (InfoProducer infoProducer : typeId == null ? this.infoProducers.getAllProducers() - : this.infoProducers.getProducersForType(typeId)) { + for (InfoProducer infoProducer : getProducers(typeId)) { result.add(infoProducer.getId()); } return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK); } + private Collection getProducers(String typeId) throws ServiceException { + if (typeId == null) { + return this.infoProducers.getAllProducers(); + } + InfoType type = infoTypes.get(typeId); + if (type == null) { + return new ArrayList<>(); + } + return infoProducers.getProducersSupportingType(infoTypes.getType(typeId)); + } + @GetMapping( path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}", produces = MediaType.APPLICATION_JSON_VALUE) @@ -422,15 +436,15 @@ public class ProducerController { return new ProducerInfoTypeInfo(t.getJobDataSchema(), t.getTypeSpecificInfo()); } - private InfoProducers.InfoProducerRegistrationInfo toProducerRegistrationInfo(String infoProducerId, + private InfoProducerRegistrationInfo toProducerRegistrationInfo(String infoProducerId, ProducerRegistrationInfo info) throws ServiceException { - Collection supportedTypes = new ArrayList<>(); + Set supportedTypes = new HashSet<>(); for (String typeId : info.supportedTypeIds) { InfoType type = this.infoTypes.getType(typeId); supportedTypes.add(type); } - return InfoProducers.InfoProducerRegistrationInfo.builder() // + return InfoProducerRegistrationInfo.builder() // .id(infoProducerId) // .jobCallbackUrl(info.jobCallbackUrl) // .producerSupervisionCallbackUrl(info.producerSupervisionCallbackUrl) // diff --git a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerJobInfo.java b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerJobInfo.java index 1dd2afb..bbaa438 100644 --- a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerJobInfo.java +++ b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerJobInfo.java @@ -73,7 +73,8 @@ public class ProducerJobInfo { } public ProducerJobInfo(InfoJob job) { - this(job.getJobData(), job.getId(), job.getTypeId(), job.getTargetUrl(), job.getOwner(), job.getLastUpdated()); + this(job.getJobData(), job.getId(), job.getType().getId(), job.getTargetUrl(), job.getOwner(), + job.getLastUpdated()); } public ProducerJobInfo() { diff --git a/src/main/java/org/oransc/ics/repository/InfoJob.java b/src/main/java/org/oransc/ics/repository/InfoJob.java index ffecdc9..87187b2 100644 --- a/src/main/java/org/oransc/ics/repository/InfoJob.java +++ b/src/main/java/org/oransc/ics/repository/InfoJob.java @@ -24,6 +24,7 @@ import java.lang.invoke.MethodHandles; import java.time.Instant; import lombok.Builder; +import lombok.EqualsAndHashCode; import lombok.Getter; import org.slf4j.Logger; @@ -40,7 +41,7 @@ public class InfoJob { private final String id; @Getter - private final String typeId; + private final InfoType type; @Getter private final String owner; @@ -62,9 +63,47 @@ public class InfoJob { @Builder.Default private boolean isLastStatusReportedEnabled = true; + @Getter + @Builder + @EqualsAndHashCode + public static class PersistentData { + private String id; + private String typeId; + private String owner; + private Object jobData; + private String targetUrl; + private String jobStatusUrl; + private String lastUpdated; + } + public void setLastReportedStatus(boolean isEnabled) { this.isLastStatusReportedEnabled = isEnabled; logger.debug("Job status id: {}, enabled: {}", this.isLastStatusReportedEnabled, isEnabled); } + public PersistentData getPersistentData() { + return PersistentData.builder() // + .id(id) // + .jobData(jobData) // + .jobStatusUrl(jobStatusUrl) // + .owner(owner) // + .targetUrl(targetUrl) // + .typeId(type.getId()) // + .lastUpdated(lastUpdated) // + .build(); + } + + @Override + public int hashCode() { + return this.id.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof InfoJob) { + return this.id.equals(((InfoJob) o).id); + } + return this.id.equals(o); + } + } diff --git a/src/main/java/org/oransc/ics/repository/InfoJobs.java b/src/main/java/org/oransc/ics/repository/InfoJobs.java index 1654e1f..8f8e0e9 100644 --- a/src/main/java/org/oransc/ics/repository/InfoJobs.java +++ b/src/main/java/org/oransc/ics/repository/InfoJobs.java @@ -52,21 +52,23 @@ import org.springframework.util.FileSystemUtils; public class InfoJobs { private Map allEiJobs = new HashMap<>(); - private MultiMap jobsByType = new MultiMap<>(); - private MultiMap jobsByOwner = new MultiMap<>(); + private MultiMap jobsByType = new MultiMap<>(); + private MultiMap jobsByOwner = new MultiMap<>(); private final Gson gson; + private final InfoTypes infoTypes; private final ApplicationConfig config; private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final ProducerCallbacks producerCallbacks; - public InfoJobs(ApplicationConfig config, ProducerCallbacks producerCallbacks) { + public InfoJobs(ApplicationConfig config, InfoTypes infoTypes, ProducerCallbacks producerCallbacks) { this.config = config; GsonBuilder gsonBuilder = new GsonBuilder(); ServiceLoader.load(TypeAdapterFactory.class).forEach(gsonBuilder::registerTypeAdapterFactory); this.gson = gsonBuilder.create(); this.producerCallbacks = producerCallbacks; + this.infoTypes = infoTypes; } public synchronized void restoreJobsFromDatabase() throws IOException { @@ -75,11 +77,29 @@ public class InfoJobs { for (File file : dbDir.listFiles()) { String json = Files.readString(file.toPath()); - InfoJob job = gson.fromJson(json, InfoJob.class); - this.doPut(job); + InfoJob.PersistentData data = gson.fromJson(json, InfoJob.PersistentData.class); + try { + InfoJob job = toInfoJob(data); + this.doPut(job); + } catch (ServiceException e) { + logger.warn("Could not restore job:{},reason: {}", data.getId(), e.getMessage()); + } } } + private InfoJob toInfoJob(InfoJob.PersistentData data) throws ServiceException { + InfoType type = infoTypes.getType(data.getTypeId()); + return InfoJob.builder() // + .id(data.getId()) // + .type(type) // + .owner(data.getOwner()) // + .jobData(data.getJobData()) // + .targetUrl(data.getTargetUrl()) // + .jobStatusUrl(data.getJobStatusUrl()) // + .lastUpdated(data.getLastUpdated()) // + .build(); + } + public synchronized void put(InfoJob job) { this.doPut(job); storeJobInFile(job); @@ -123,8 +143,8 @@ public class InfoJobs { public synchronized void remove(InfoJob job, InfoProducers infoProducers) { this.allEiJobs.remove(job.getId()); - jobsByType.remove(job.getTypeId(), job.getId()); - jobsByOwner.remove(job.getOwner(), job.getId()); + jobsByType.remove(job.getType().getId(), job); + jobsByOwner.remove(job.getOwner(), job); try { Files.delete(getPath(job)); @@ -132,6 +152,7 @@ public class InfoJobs { logger.warn("Could not remove file: {}", e.getMessage()); } this.producerCallbacks.stopInfoJob(job, infoProducers); + } public synchronized int size() { @@ -156,14 +177,14 @@ public class InfoJobs { private void doPut(InfoJob job) { allEiJobs.put(job.getId(), job); - jobsByType.put(job.getTypeId(), job.getId(), job); - jobsByOwner.put(job.getOwner(), job.getId(), job); + jobsByType.put(job.getType().getId(), job); + jobsByOwner.put(job.getOwner(), job); } private void storeJobInFile(InfoJob job) { try { try (PrintStream out = new PrintStream(new FileOutputStream(getFile(job)))) { - out.print(gson.toJson(job)); + out.print(gson.toJson(job.getPersistentData())); } } catch (Exception e) { logger.warn("Could not store job: {} {}", job.getId(), e.getMessage()); diff --git a/src/main/java/org/oransc/ics/repository/InfoProducer.java b/src/main/java/org/oransc/ics/repository/InfoProducer.java index 01ec263..3fa278e 100644 --- a/src/main/java/org/oransc/ics/repository/InfoProducer.java +++ b/src/main/java/org/oransc/ics/repository/InfoProducer.java @@ -20,7 +20,6 @@ package org.oransc.ics.repository; -import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -31,7 +30,7 @@ public class InfoProducer { private final String id; @Getter - private final Collection infoTypes; + private final Set infoTypes; @Getter private final String jobCallbackUrl; @@ -43,7 +42,7 @@ public class InfoProducer { private int unresponsiveCounter = 0; - public InfoProducer(String id, Collection infoTypes, String jobCallbackUrl, + public InfoProducer(String id, Set infoTypes, String jobCallbackUrl, String producerSupervisionCallbackUrl) { this.id = id; this.infoTypes = infoTypes; @@ -82,4 +81,17 @@ public class InfoProducer { return this.enabledJobs.contains(job.getId()); } + @Override + public int hashCode() { + return this.id.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof InfoProducer) { + return this.id.equals(((InfoProducer) o).id); + } + return this.id.equals(o); + } + } diff --git a/src/main/java/org/oransc/ics/repository/InfoProducerRegistrationInfo.java b/src/main/java/org/oransc/ics/repository/InfoProducerRegistrationInfo.java new file mode 100644 index 0000000..7aafdfa --- /dev/null +++ b/src/main/java/org/oransc/ics/repository/InfoProducerRegistrationInfo.java @@ -0,0 +1,38 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2022 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.ics.repository; + +import java.util.Set; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class InfoProducerRegistrationInfo { + String id; + + Set supportedTypes; + + String jobCallbackUrl; + + String producerSupervisionCallbackUrl; +} diff --git a/src/main/java/org/oransc/ics/repository/InfoProducers.java b/src/main/java/org/oransc/ics/repository/InfoProducers.java index 19b2698..4ea881f 100644 --- a/src/main/java/org/oransc/ics/repository/InfoProducers.java +++ b/src/main/java/org/oransc/ics/repository/InfoProducers.java @@ -27,9 +27,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Vector; -import lombok.Builder; -import lombok.Getter; - import org.oransc.ics.controllers.a1e.A1eCallbacks; import org.oransc.ics.controllers.r1producer.ProducerCallbacks; import org.oransc.ics.exceptions.ServiceException; @@ -46,8 +43,7 @@ import org.springframework.stereotype.Component; @Component public class InfoProducers { private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private final Map allEiProducers = new HashMap<>(); - private final MultiMap producersByType = new MultiMap<>(); + private final Map allInfoProducers = new HashMap<>(); @Autowired private ProducerCallbacks producerCallbacks; @@ -58,33 +54,15 @@ public class InfoProducers { @Autowired private InfoJobs infoJobs; - @Builder - @Getter - public static class InfoProducerRegistrationInfo { - String id; - - Collection supportedTypes; - - String jobCallbackUrl; - - String producerSupervisionCallbackUrl; - } + @Autowired + private InfoTypes infoTypes; public InfoProducer registerProducer(InfoProducerRegistrationInfo producerInfo) { final String producerId = producerInfo.getId(); InfoProducer previousDefinition = this.get(producerId); - if (previousDefinition != null) { - for (InfoType type : previousDefinition.getInfoTypes()) { - producersByType.remove(type.getId(), producerId); - } - allEiProducers.remove(producerId); - } InfoProducer producer = createProducer(producerInfo); - allEiProducers.put(producer.getId(), producer); - for (InfoType type : producer.getInfoTypes()) { - producersByType.put(type.getId(), producer.getId(), producer); - } + allInfoProducers.put(producer.getId(), producer); Collection previousTypes = previousDefinition != null ? previousDefinition.getInfoTypes() : new ArrayList<>(); @@ -105,11 +83,11 @@ public class InfoProducers { } public synchronized Collection getAllProducers() { - return new Vector<>(allEiProducers.values()); + return new Vector<>(allInfoProducers.values()); } public synchronized InfoProducer getProducer(String id) throws ServiceException { - InfoProducer p = allEiProducers.get(id); + InfoProducer p = allInfoProducers.get(id); if (p == null) { throw new ServiceException("Could not find Information Producer: " + id, HttpStatus.NOT_FOUND); } @@ -117,50 +95,57 @@ public class InfoProducers { } public synchronized InfoProducer get(String id) { - return allEiProducers.get(id); + return allInfoProducers.get(id); } public synchronized int size() { - return allEiProducers.size(); + return allInfoProducers.size(); } public synchronized void clear() { - this.allEiProducers.clear(); - this.producersByType.clear(); + this.allInfoProducers.clear(); } public void deregisterProducer(InfoProducer producer) { - allEiProducers.remove(producer.getId()); - for (InfoType type : producer.getInfoTypes()) { - if (producersByType.remove(type.getId(), producer.getId()) == null) { - this.logger.error("Bug, no producer found"); - } - } + allInfoProducers.remove(producer.getId()); this.consumerCallbacks.notifyJobStatus(producer.getInfoTypes(), this) // .subscribe(); - } - public synchronized Collection getProducersForType(InfoType type) { - return this.producersByType.get(type.getId()); } - public synchronized Collection getProducersForType(String typeId) { - return this.producersByType.get(typeId); + public synchronized Collection getProducersSupportingType(InfoType type) { + InfoType.TypeId id = type.getTypeId(); + Collection result = new ArrayList<>(); + for (InfoProducer producer : this.allInfoProducers.values()) { + if (producer.getInfoTypes().contains(type) + || !InfoType.getCompatibleTypes(producer.getInfoTypes(), id).isEmpty()) { + result.add(producer); + } + } + + return result; } - public synchronized Collection getProducerIdsForType(String typeId) { + public synchronized Collection getProducerIdsForType(InfoType type) { Collection producerIds = new ArrayList<>(); - for (InfoProducer p : this.getProducersForType(typeId)) { + for (InfoProducer p : this.getProducersSupportingType(type)) { producerIds.add(p.getId()); } return producerIds; } public synchronized boolean isJobEnabled(InfoJob job) { - for (InfoProducer producer : this.producersByType.get(job.getTypeId())) { - if (producer.isJobEnabled(job)) { - return true; + InfoType type; + try { + type = this.infoTypes.getType(job.getType().getId()); + + for (InfoProducer producer : this.getProducersSupportingType(type)) { + if (producer.isJobEnabled(job)) { + return true; + } } + } catch (ServiceException e) { + logger.error("Unexpected execption: {}", e.getMessage()); } return false; } diff --git a/src/main/java/org/oransc/ics/repository/InfoType.java b/src/main/java/org/oransc/ics/repository/InfoType.java index 1a3f6e5..db7e26b 100644 --- a/src/main/java/org/oransc/ics/repository/InfoType.java +++ b/src/main/java/org/oransc/ics/repository/InfoType.java @@ -20,11 +20,134 @@ package org.oransc.ics.repository; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import lombok.Builder; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; +import org.oransc.ics.exceptions.ServiceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; + @ToString public class InfoType { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + @Builder + @Getter + @EqualsAndHashCode + public static class PersistentInfo { + @Getter + private String id; + + @Getter + private Object jobDataSchema; + + @Getter + private Object typeSpecificInfo; + } + + @Getter + @ToString + public static class Version { + public final int major; + public final int minor; + public final int patch; + + public Version(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + public static Version ofString(String version) throws ServiceException { + String[] versionTokenized = version.split("\\."); + if (versionTokenized.length != 3) { + throw new ServiceException("Version must contain major.minor.patch code: " + version, + HttpStatus.BAD_REQUEST); + } + + try { + return new Version( // + Integer.parseInt(versionTokenized[0]), // + Integer.parseInt(versionTokenized[1]), // + Integer.parseInt(versionTokenized[2]) // + ); + } catch (Exception e) { + throw new ServiceException("Syntax error in " + version, HttpStatus.BAD_REQUEST); + } + } + + public int compareTo(Version other) { + if (major != other.major) + return major - other.major; + if (minor != other.minor) + return minor - other.minor; + return patch - other.patch; + } + + public boolean isCompatibleWith(Version other) { + return (major == other.major && minor >= other.minor); + } + } + + @ToString + @Getter + public static class TypeId { + private final String name; + private final String version; + + public TypeId(String name, String version) { + this.name = name; + this.version = version; + } + + public static TypeId ofString(String typeId) { + StringBuilder name = new StringBuilder(); + String version = ""; + String[] tokens = typeId.split("_"); + + if (tokens.length >= 2) { + version = tokens[tokens.length - 1]; // Last token + for (int i = 0; i < tokens.length - 1; ++i) { + if (i != 0) { + name.append("_"); + } + name.append(tokens[i]); // All other tokens + } + return new TypeId(name.toString(), version); + } else { + return new TypeId(typeId, ""); + } + } + } + + public static Collection getCompatibleTypes(Collection allTypes, TypeId requestedTypeId) { + List result = new ArrayList<>(); + try { + InfoType.Version requestedVersion = InfoType.Version.ofString(requestedTypeId.version); + for (InfoType type : allTypes) { + TypeId typeId = type.getTypeId(); + if (requestedTypeId.getName().equals(typeId.getName()) + && InfoType.Version.ofString(typeId.getVersion()).isCompatibleWith(requestedVersion)) { + result.add(type); + } + } + result.sort((left, right) -> left.getVersion().compareTo(right.getVersion())); + + } catch (ServiceException e) { + logger.warn("Failed to find compatible version with: {}, reason: {}", requestedTypeId, e.getMessage()); + } + return result; + } + @Getter private final String id; @@ -40,4 +163,44 @@ public class InfoType { this.typeSpecificInfo = typeSpecificInfo; } + public InfoType(PersistentInfo info) { + this.id = info.getId(); + this.jobDataSchema = info.getJobDataSchema(); + this.typeSpecificInfo = info.getTypeSpecificInfo(); + } + + public PersistentInfo getPersistentInfo() { + return PersistentInfo.builder() // + .id(this.id) // + .jobDataSchema(this.jobDataSchema)// + .typeSpecificInfo(this.typeSpecificInfo) // + .build(); + } + + public TypeId getTypeId() { + return TypeId.ofString(getId()); + } + + public Version getVersion() { + try { + return Version.ofString(getTypeId().getVersion()); + } catch (ServiceException e) { + logger.warn("Not possible to get version from info type ID {}, {}", getId(), e.getMessage()); + return new Version(0, 0, 0); + } + } + + @Override + public int hashCode() { + return this.id.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof InfoType) { + return this.id.equals(((InfoType) o).id); + } + return this.id.equals(o); + } + } diff --git a/src/main/java/org/oransc/ics/repository/InfoTypeSubscriptions.java b/src/main/java/org/oransc/ics/repository/InfoTypeSubscriptions.java index a72a259..2557f3c 100644 --- a/src/main/java/org/oransc/ics/repository/InfoTypeSubscriptions.java +++ b/src/main/java/org/oransc/ics/repository/InfoTypeSubscriptions.java @@ -62,7 +62,7 @@ import reactor.util.retry.Retry; public class InfoTypeSubscriptions { private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final Map allSubscriptions = new HashMap<>(); - private final MultiMap subscriptionsByOwner = new MultiMap<>(); + private final MultiMap subscriptionsByOwner = new MultiMap<>(); private final Gson gson = new GsonBuilder().create(); private final ApplicationConfig config; private final Map callbackHandlers = new HashMap<>(); @@ -83,6 +83,20 @@ public class InfoTypeSubscriptions { private String owner; private String apiVersion; + + @Override + public int hashCode() { + return this.id.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof SubscriptionInfo) { + return this.id.equals(((SubscriptionInfo) o).id); + } + return this.id.equals(o); + } + } public InfoTypeSubscriptions(@Autowired ApplicationConfig config) { @@ -111,7 +125,7 @@ public class InfoTypeSubscriptions { /** * Get a subscription and throw if not fond. - * + * * @param id the ID of the subscription to get. * @return SubscriptionInfo * @throws ServiceException if not found @@ -127,7 +141,7 @@ public class InfoTypeSubscriptions { /** * Get a subscription or return null if not found. Equivalent to get in all java * collections. - * + * * @param id the ID of the subscription to get. * @return SubscriptionInfo */ @@ -147,7 +161,7 @@ public class InfoTypeSubscriptions { public void remove(SubscriptionInfo subscription) { allSubscriptions.remove(subscription.getId()); - subscriptionsByOwner.remove(subscription.owner, subscription.id); + subscriptionsByOwner.remove(subscription.owner, subscription); try { Files.delete(getPath(subscription)); @@ -214,7 +228,7 @@ public class InfoTypeSubscriptions { /** * Invoking one consumer. If the call fails after retries, the subscription is * removed. - * + * * @param notifyFunc * @param subscriptionInfo * @return @@ -265,7 +279,7 @@ public class InfoTypeSubscriptions { private void doPut(SubscriptionInfo subscription) { allSubscriptions.put(subscription.getId(), subscription); - subscriptionsByOwner.put(subscription.owner, subscription.id, subscription); + subscriptionsByOwner.put(subscription.owner, subscription); } private File getFile(SubscriptionInfo subscription) { diff --git a/src/main/java/org/oransc/ics/repository/InfoTypes.java b/src/main/java/org/oransc/ics/repository/InfoTypes.java index 9d2abc2..110bca2 100644 --- a/src/main/java/org/oransc/ics/repository/InfoTypes.java +++ b/src/main/java/org/oransc/ics/repository/InfoTypes.java @@ -51,7 +51,7 @@ import org.springframework.util.FileSystemUtils; @SuppressWarnings("squid:S2629") // Invoke method(s) only conditionally public class InfoTypes { private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private final Map allEiTypes = new HashMap<>(); + private final Map allInfoTypes = new HashMap<>(); private final ApplicationConfig config; private final Gson gson; @@ -68,22 +68,23 @@ public class InfoTypes { for (File file : dbDir.listFiles()) { String json = Files.readString(file.toPath()); - InfoType type = gson.fromJson(json, InfoType.class); - allEiTypes.put(type.getId(), type); + InfoType.PersistentInfo storedData = gson.fromJson(json, InfoType.PersistentInfo.class); + InfoType type = new InfoType(storedData); + allInfoTypes.put(type.getId(), type); } } public synchronized void put(InfoType type) { - allEiTypes.put(type.getId(), type); + allInfoTypes.put(type.getId(), type); storeInFile(type); } public synchronized Collection getAllInfoTypes() { - return new Vector<>(allEiTypes.values()); + return new Vector<>(allInfoTypes.values()); } public synchronized InfoType getType(String id) throws ServiceException { - InfoType type = allEiTypes.get(id); + InfoType type = allInfoTypes.get(id); if (type == null) { throw new ServiceException("Information type not found: " + id, HttpStatus.NOT_FOUND); } @@ -91,11 +92,11 @@ public class InfoTypes { } public synchronized InfoType get(String id) { - return allEiTypes.get(id); + return allInfoTypes.get(id); } public synchronized void remove(InfoType type) { - allEiTypes.remove(type.getId()); + allInfoTypes.remove(type.getId()); try { Files.delete(getPath(type)); } catch (IOException e) { @@ -104,14 +105,28 @@ public class InfoTypes { } public synchronized int size() { - return allEiTypes.size(); + return allInfoTypes.size(); } public synchronized void clear() { - this.allEiTypes.clear(); + this.allInfoTypes.clear(); clearDatabase(); } + public synchronized InfoType getCompatibleType(String typeId) throws ServiceException { + InfoType res = this.get(typeId); + if (res != null) { + return res; + } + + Collection compatibleTypes = + InfoType.getCompatibleTypes(this.getAllInfoTypes(), InfoType.TypeId.ofString(typeId)); + if (compatibleTypes.isEmpty()) { + throw new ServiceException("Information type not found: " + typeId, HttpStatus.NOT_FOUND); + } + return compatibleTypes.iterator().next(); + } + private void clearDatabase() { try { FileSystemUtils.deleteRecursively(Path.of(getDatabaseDirectory())); @@ -124,7 +139,7 @@ public class InfoTypes { private void storeInFile(InfoType type) { try { try (PrintStream out = new PrintStream(new FileOutputStream(getFile(type)))) { - out.print(gson.toJson(type)); + out.print(gson.toJson(type.getPersistentInfo())); } } catch (Exception e) { logger.warn("Could not save type: {} {}", type.getId(), e.getMessage()); diff --git a/src/main/java/org/oransc/ics/repository/MultiMap.java b/src/main/java/org/oransc/ics/repository/MultiMap.java index 0f3be0a..0ba57b7 100644 --- a/src/main/java/org/oransc/ics/repository/MultiMap.java +++ b/src/main/java/org/oransc/ics/repository/MultiMap.java @@ -20,45 +20,46 @@ package org.oransc.ics.repository; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; -import java.util.Vector; +import java.util.Set; /** - * A map, where each key can be bound to may values (where each value has an own - * ID) + * A map, where each key can be bound to may values */ -public class MultiMap { +public class MultiMap { - private final Map> map = new HashMap<>(); + private final Map> map = new HashMap<>(); - public void put(String key, String id, T value) { - this.map.computeIfAbsent(key, k -> new HashMap<>()).put(id, value); + public synchronized void put(K key, V value) { + this.map.computeIfAbsent(key, k -> new HashSet<>()).add(value); } - public T remove(String key, String id) { - Map innerMap = this.map.get(key); + public synchronized void remove(String key, V id) { + Set innerMap = this.map.get(key); if (innerMap != null) { - T removedElement = innerMap.remove(id); + innerMap.remove(id); if (innerMap.isEmpty()) { this.map.remove(key); } - return removedElement; } - return null; } - public Collection get(String key) { - Map innerMap = this.map.get(key); + public synchronized Collection get(K key) { + Set innerMap = this.map.get(key); if (innerMap == null) { return Collections.emptyList(); } - return new Vector<>(innerMap.values()); + Collection result = new ArrayList<>(innerMap.size()); + result.addAll(innerMap); + return result; } - public void clear() { + public synchronized void clear() { this.map.clear(); } diff --git a/src/test/java/org/oransc/ics/ApplicationTest.java b/src/test/java/org/oransc/ics/ApplicationTest.java index 73a79ee..77382b7 100644 --- a/src/test/java/org/oransc/ics/ApplicationTest.java +++ b/src/test/java/org/oransc/ics/ApplicationTest.java @@ -107,7 +107,7 @@ import reactor.test.StepVerifier; class ApplicationTest { private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private final String TYPE_ID = "typeId"; + private final String TYPE_ID = "typeId_1.9.9"; private final String PRODUCER_ID = "producerId"; private final String EI_JOB_PROPERTY = "\"property1\""; private final String EI_JOB_ID = "jobId"; @@ -234,7 +234,6 @@ class ApplicationTest { assertThat(this.producerSimulator.getTestResults().jobsStarted).hasSize(3); await().untilAsserted(() -> assertThat(this.producerSimulator.getTestResults().jobsStopped).hasSize(2)); - } @Test @@ -289,7 +288,7 @@ class ApplicationTest { putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID); putInfoJob(TYPE_ID, "jobId"); final String JOB_ID_JSON = "[\"jobId\"]"; - String url = A1eConsts.API_ROOT + "/eijobs?infoTypeId=typeId"; + String url = A1eConsts.API_ROOT + "/eijobs?infoTypeId=" + TYPE_ID; String rsp = restClient().get(url).block(); assertThat(rsp).isEqualTo(JOB_ID_JSON); @@ -305,7 +304,7 @@ class ApplicationTest { rsp = restClient().get(url).block(); assertThat(rsp).isEqualTo(JOB_ID_JSON); - url = A1eConsts.API_ROOT + "/eijobs?eiTypeId=typeId&&owner=owner"; + url = A1eConsts.API_ROOT + "/eijobs?eiTypeId=" + TYPE_ID + "&&owner=owner"; rsp = restClient().get(url).block(); assertThat(rsp).isEqualTo(JOB_ID_JSON); @@ -319,7 +318,7 @@ class ApplicationTest { putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID); putInfoJob(TYPE_ID, "jobId"); final String JOB_ID_JSON = "[\"jobId\"]"; - String url = ConsumerConsts.API_ROOT + "/info-jobs?infoTypeId=typeId"; + String url = ConsumerConsts.API_ROOT + "/info-jobs?infoTypeId=" + TYPE_ID; String rsp = restClient().get(url).block(); assertThat(rsp).isEqualTo(JOB_ID_JSON); @@ -335,7 +334,7 @@ class ApplicationTest { rsp = restClient().get(url).block(); assertThat(rsp).isEqualTo(JOB_ID_JSON); - url = ConsumerConsts.API_ROOT + "/info-jobs?infoTypeId=typeId&&owner=owner"; + url = ConsumerConsts.API_ROOT + "/info-jobs?infoTypeId=" + TYPE_ID + "&&owner=owner"; rsp = restClient().get(url).block(); assertThat(rsp).isEqualTo(JOB_ID_JSON); @@ -450,12 +449,13 @@ class ApplicationTest { @Test void a1ePutEiJob() throws Exception { + // Test that one producer accepting a job is enough putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID); putInfoProducerWithOneTypeRejecting("simulateProducerError", TYPE_ID); String url = A1eConsts.API_ROOT + "/eijobs/jobId"; - String body = gson.toJson(infoJobInfo()); + String body = gson.toJson(eiJobInfo()); ResponseEntity resp = restClient().putForEntity(url, body).block(); assertThat(this.infoJobs.size()).isEqualTo(1); assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED); @@ -477,6 +477,33 @@ class ApplicationTest { verifyJobStatus(EI_JOB_ID, "ENABLED"); } + @Test + void a1ePutEiJobVersionHandling() throws Exception { + final String REG_TYPE_ID1 = "type_1.5.0"; // Compatible + final String REG_TYPE_ID2 = "type_1.6.0"; // Compatible + final String REG_TYPE_ID3 = "type_2.0.0"; + final String REG_TYPE_ID4 = "type_1.0.0"; + final String REG_TYPE_ID5 = "xxxx_1.3.0"; // Other type + + final String PUT_TYPE_ID = "type_1.1.0"; + // Test that one producer accepting a job is enough + putInfoProducerWithOneType(REG_TYPE_ID1, REG_TYPE_ID1); + putInfoProducerWithOneType(REG_TYPE_ID2, REG_TYPE_ID2); + putInfoProducerWithOneType(REG_TYPE_ID3, REG_TYPE_ID3); + putInfoProducerWithOneType(REG_TYPE_ID4, REG_TYPE_ID4); + putInfoProducerWithOneType(REG_TYPE_ID5, REG_TYPE_ID5); + + String url = A1eConsts.API_ROOT + "/eijobs/jobId"; + String body = gson.toJson(eiJobInfo(PUT_TYPE_ID, EI_JOB_ID)); + ResponseEntity resp = restClient().putForEntity(url, body).block(); + assertThat(this.infoJobs.size()).isEqualTo(1); + assertThat(this.infoJobs.getJobs().iterator().next().getType().getId()).isEqualTo(REG_TYPE_ID1); + assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + resp = restClient().putForEntity(url, body).block(); + assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK); + } + @Test void consumerPutInformationJob() throws Exception { // Test that one producer accepting a job is enough @@ -501,14 +528,59 @@ class ApplicationTest { verifyJobStatus(EI_JOB_ID, "ENABLED"); } + @Test + void testVersioning() throws Exception { + final String REG_TYPE_ID1 = "type_1.5.0"; // Compatible + final String REG_TYPE_ID2 = "type_1.6.0"; // Compatible + final String REG_TYPE_ID3 = "type_2.0.0"; + final String REG_TYPE_ID4 = "type_1.0.0"; + final String REG_TYPE_ID5 = "xxxx_1.3.0"; // Other type + + final String PUT_TYPE_ID = "type_1.1.0"; + // Test that one producer accepting a job is enough + putInfoProducerWithOneType(REG_TYPE_ID1, REG_TYPE_ID1); + putInfoProducerWithOneType(REG_TYPE_ID2, REG_TYPE_ID2); + putInfoProducerWithOneType(REG_TYPE_ID3, REG_TYPE_ID3); + putInfoProducerWithOneType(REG_TYPE_ID4, REG_TYPE_ID4); + putInfoProducerWithOneType(REG_TYPE_ID5, REG_TYPE_ID5); + + String url = ConsumerConsts.API_ROOT + "/info-jobs/jobId"; + String body = gson.toJson(consumerJobInfo(PUT_TYPE_ID, "jobId")); + ResponseEntity resp = restClient().putForEntity(url, body).block(); + assertThat(this.infoJobs.size()).isEqualTo(1); + assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + ConsumerJobInfo getInfo = gson.fromJson(restClient().get(url).block(), ConsumerJobInfo.class); + assertThat(getInfo.infoTypeId).isEqualTo(REG_TYPE_ID1); + + ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults(); + await().untilAsserted(() -> assertThat(simulatorResults.jobsStarted).hasSize(2)); + ProducerJobInfo request = simulatorResults.jobsStarted.get(0); + assertThat(request.id).isEqualTo("jobId"); + + // TBD the producer gets the registerred type, but could get the requested + // (PUT_TYPE_ID). This + // depends on the + // the principles for backwards compability. + assertThat(request.typeId.equals(REG_TYPE_ID1) || request.typeId.equals(REG_TYPE_ID2)).isTrue(); + + verifyJobStatus(EI_JOB_ID, "ENABLED"); + + // Test update job + resp = restClient().putForEntity(url, body).block(); + assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK); + await().untilAsserted(() -> assertThat(simulatorResults.jobsStarted).hasSize(4)); + + } + @Test void a1ePutEiJob_jsonSchemavalidationError() throws Exception { putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID); String url = A1eConsts.API_ROOT + "/eijobs/jobId"; // The element with name "property1" is mandatory in the schema - A1eEiJobInfo jobInfo = new A1eEiJobInfo("typeId", jsonObject("{ \"XXstring\" : \"value\" }"), "owner", - "targetUri", "jobStatusUrl"); + A1eEiJobInfo jobInfo = + new A1eEiJobInfo(TYPE_ID, jsonObject("{ \"XXstring\" : \"value\" }"), "owner", "targetUri", "jobStatusUrl"); String body = gson.toJson(jobInfo); testErrorCode(restClient().put(url, body), HttpStatus.BAD_REQUEST, "Json validation failure"); @@ -524,7 +596,7 @@ class ApplicationTest { String url = ConsumerConsts.API_ROOT + "/info-jobs/jobId"; // The element with name "property1" is mandatory in the schema ConsumerJobInfo jobInfo = - new ConsumerJobInfo("typeId", jsonObject("{ \"XXstring\" : \"value\" }"), "owner", "targetUri", null); + new ConsumerJobInfo(TYPE_ID, jsonObject("{ \"XXstring\" : \"value\" }"), "owner", "targetUri", null); String body = gson.toJson(jobInfo); testErrorCode(restClient().put(url, body), HttpStatus.BAD_REQUEST, "Json validation failure"); @@ -549,7 +621,7 @@ class ApplicationTest { putInfoJob("typeId1", "jobId"); String url = A1eConsts.API_ROOT + "/eijobs/jobId"; - String body = gson.toJson(infoJobInfo("typeId2", "jobId")); + String body = gson.toJson(eiJobInfo("typeId2", "jobId")); testErrorCode(restClient().put(url, body), HttpStatus.CONFLICT, "Not allowed to change type for existing EI job"); } @@ -566,13 +638,13 @@ class ApplicationTest { } @Test - void producerPutEiType() throws JsonMappingException, JsonProcessingException, ServiceException { + void producerPutType() throws JsonMappingException, JsonProcessingException, ServiceException { assertThat(putInfoType(TYPE_ID)).isEqualTo(HttpStatus.CREATED); assertThat(putInfoType(TYPE_ID)).isEqualTo(HttpStatus.OK); } @Test - void producerPutEiType_noSchema() { + void producerPutType_noSchema() { String url = ProducerConsts.API_ROOT + "/info-types/" + TYPE_ID; String body = "{}"; testErrorCode(restClient().put(url, body), HttpStatus.BAD_REQUEST, "No schema provided"); @@ -581,7 +653,7 @@ class ApplicationTest { } @Test - void producerDeleteEiType() throws Exception { + void producerDeleteType() throws Exception { putInfoType(TYPE_ID); this.putInfoJob(TYPE_ID, "job1"); this.putInfoJob(TYPE_ID, "job2"); @@ -595,18 +667,30 @@ class ApplicationTest { } @Test - void producerDeleteEiTypeExistingProducer() throws Exception { + void producerDeleteTypeExistingProducer() throws Exception { putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID); String url = ProducerConsts.API_ROOT + "/info-types/" + TYPE_ID; testErrorCode(restClient().delete(url), HttpStatus.CONFLICT, "The type has active producers: " + PRODUCER_ID); assertThat(this.infoTypes.size()).isEqualTo(1); } + @Test + void producerDeleteTypeExistingJob() throws Exception { + putInfoType(TYPE_ID); + String url = ProducerConsts.API_ROOT + "/info-types/" + TYPE_ID; + putInfoJob(TYPE_ID, EI_JOB_ID); + restClient().delete(url).block(); + assertThat(this.infoTypes.size()).isZero(); + + // The jobs are implicitly deleted + assertThat(this.infoJobs.size()).isZero(); + } + @Test void producerPutProducerWithOneType_rejecting() throws Exception { putInfoProducerWithOneTypeRejecting("simulateProducerError", TYPE_ID); String url = A1eConsts.API_ROOT + "/eijobs/" + EI_JOB_ID; - String body = gson.toJson(infoJobInfo()); + String body = gson.toJson(eiJobInfo()); restClient().put(url, body).block(); ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults(); @@ -642,7 +726,8 @@ class ApplicationTest { assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(this.infoTypes.size()).isEqualTo(1); - assertThat(this.infoProducers.getProducersForType(TYPE_ID)).hasSize(1); + InfoType type = this.infoTypes.getType(TYPE_ID); + assertThat(this.infoProducers.getProducersSupportingType(type)).hasSize(1); assertThat(this.infoProducers.size()).isEqualTo(1); assertThat(this.infoProducers.get("infoProducerId").getInfoTypes().iterator().next().getId()) .isEqualTo(TYPE_ID); @@ -689,7 +774,7 @@ class ApplicationTest { this.infoTypes.getType(TYPE_ID); url = A1eConsts.API_ROOT + "/eijobs/jobId"; - body = gson.toJson(infoJobInfo()); + body = gson.toJson(eiJobInfo()); restClient().putForEntity(url, body).block(); ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults(); @@ -725,14 +810,14 @@ class ApplicationTest { assertThat(this.infoProducers.size()).isEqualTo(2); InfoType type = this.infoTypes.getType(TYPE_ID); - assertThat(this.infoProducers.getProducerIdsForType(type.getId())).contains("infoProducerId"); - assertThat(this.infoProducers.getProducerIdsForType(type.getId())).contains("infoProducerId2"); + assertThat(this.infoProducers.getProducerIdsForType(type)).contains("infoProducerId"); + assertThat(this.infoProducers.getProducerIdsForType(type)).contains("infoProducerId2"); putInfoJob(TYPE_ID, "jobId"); assertThat(this.infoJobs.size()).isEqualTo(1); deleteInfoProducer("infoProducerId"); assertThat(this.infoProducers.size()).isEqualTo(1); - assertThat(this.infoProducers.getProducerIdsForType(TYPE_ID)).doesNotContain("infoProducerId"); + assertThat(this.infoProducers.getProducerIdsForType(type)).doesNotContain("infoProducerId"); verifyJobStatus("jobId", "ENABLED"); deleteInfoProducer("infoProducerId2"); @@ -898,7 +983,7 @@ class ApplicationTest { } @Test - void testEiJobDatabase() throws Exception { + void testJobDatabasePersistence() throws Exception { putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID); putInfoJob(TYPE_ID, "jobId1"); putInfoJob(TYPE_ID, "jobId2"); @@ -908,10 +993,11 @@ class ApplicationTest { { InfoJob savedJob = this.infoJobs.getJob("jobId1"); // Restore the jobs - InfoJobs jobs = new InfoJobs(this.applicationConfig, this.producerCallbacks); + InfoJobs jobs = new InfoJobs(this.applicationConfig, this.infoTypes, this.producerCallbacks); jobs.restoreJobsFromDatabase(); assertThat(jobs.size()).isEqualTo(2); InfoJob restoredJob = jobs.getJob("jobId1"); + assertThat(restoredJob.getPersistentData()).isEqualTo(savedJob.getPersistentData()); assertThat(restoredJob.getId()).isEqualTo("jobId1"); assertThat(restoredJob.getLastUpdated()).isEqualTo(savedJob.getLastUpdated()); @@ -920,7 +1006,7 @@ class ApplicationTest { } { // Restore the jobs, no jobs in database - InfoJobs jobs = new InfoJobs(this.applicationConfig, this.producerCallbacks); + InfoJobs jobs = new InfoJobs(this.applicationConfig, this.infoTypes, this.producerCallbacks); jobs.restoreJobsFromDatabase(); assertThat(jobs.size()).isZero(); } @@ -933,23 +1019,26 @@ class ApplicationTest { } @Test - void testEiTypesDatabase() throws Exception { + void testTypesDatabasePersistence() throws Exception { putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID); + InfoType savedType = this.infoTypes.getType(TYPE_ID); assertThat(this.infoTypes.size()).isEqualTo(1); { // Restore the types - InfoTypes types = new InfoTypes(this.applicationConfig); - types.restoreTypesFromDatabase(); - assertThat(types.size()).isEqualTo(1); + InfoTypes restoredTypes = new InfoTypes(this.applicationConfig); + restoredTypes.restoreTypesFromDatabase(); + InfoType restoredType = restoredTypes.getType(TYPE_ID); + assertThat(restoredType.getPersistentInfo()).isEqualTo(savedType.getPersistentInfo()); + assertThat(restoredTypes.size()).isEqualTo(1); } { // Restore the jobs, no jobs in database - InfoTypes types = new InfoTypes(this.applicationConfig); - types.clear(); - types.restoreTypesFromDatabase(); - assertThat(types.size()).isZero(); + InfoTypes restoredTypes = new InfoTypes(this.applicationConfig); + restoredTypes.clear(); + restoredTypes.restoreTypesFromDatabase(); + assertThat(restoredTypes.size()).isZero(); } logger.warn("Test removing a job when the db file is gone"); this.infoTypes.remove(this.infoTypes.getType(TYPE_ID)); @@ -1143,16 +1232,16 @@ class ApplicationTest { baseUrl() + A1eCallbacksSimulatorController.getJobStatusUrl(infoJobId)); } - private A1eEiJobInfo infoJobInfo() throws Exception { - return infoJobInfo(TYPE_ID, EI_JOB_ID); + private A1eEiJobInfo eiJobInfo() throws Exception { + return eiJobInfo(TYPE_ID, EI_JOB_ID); } - A1eEiJobInfo infoJobInfo(String typeId, String infoJobId) throws Exception { - return infoJobInfo(typeId, infoJobId, "owner"); + A1eEiJobInfo eiJobInfo(String typeId, String infoJobId) throws Exception { + return eiJobInfo(typeId, infoJobId, "owner"); } - A1eEiJobInfo infoJobInfo(String typeId, String infoJobId, String owner) throws Exception { + A1eEiJobInfo eiJobInfo(String typeId, String infoJobId, String owner) throws Exception { return new A1eEiJobInfo(typeId, jsonObject(), owner, "https://junk.com", baseUrl() + A1eCallbacksSimulatorController.getJobStatusUrl(infoJobId)); } @@ -1192,7 +1281,7 @@ class ApplicationTest { private InfoJob putInfoJob(String infoTypeId, String jobId, String owner) throws Exception { String url = A1eConsts.API_ROOT + "/eijobs/" + jobId; - String body = gson.toJson(infoJobInfo(infoTypeId, jobId, owner)); + String body = gson.toJson(eiJobInfo(infoTypeId, jobId, owner)); restClient().putForEntity(url, body).block(); return this.infoJobs.getJob(jobId); @@ -1202,7 +1291,7 @@ class ApplicationTest { private InfoJob putInfoJob(String infoTypeId, String jobId) throws Exception { String url = A1eConsts.API_ROOT + "/eijobs/" + jobId; - String body = gson.toJson(infoJobInfo(infoTypeId, jobId)); + String body = gson.toJson(eiJobInfo(infoTypeId, jobId)); restClient().putForEntity(url, body).block(); return this.infoJobs.getJob(jobId); diff --git a/src/test/java/org/oransc/ics/controller/ProducerSimulatorController.java b/src/test/java/org/oransc/ics/controller/ProducerSimulatorController.java index a5ddc49..af219f6 100644 --- a/src/test/java/org/oransc/ics/controller/ProducerSimulatorController.java +++ b/src/test/java/org/oransc/ics/controller/ProducerSimulatorController.java @@ -207,9 +207,9 @@ public class ProducerSimulatorController { } private void logHeaders(Map headers) { - logger.debug("Header begin"); - headers.forEach((key, value) -> logger.debug(" key: {}, value: {}", key, value)); - logger.debug("Header end"); + logger.trace("Header begin"); + headers.forEach((key, value) -> logger.trace(" key: {}, value: {}", key, value)); + logger.trace("Header end"); } } -- 2.16.6