NONRTRIC - A scheme for versioning of types. 36/9236/3
authorPatrikBuhr <patrik.buhr@est.tech>
Mon, 10 Oct 2022 13:08:20 +0000 (15:08 +0200)
committerPatrikBuhr <patrik.buhr@est.tech>
Wed, 12 Oct 2022 11:46:53 +0000 (13:46 +0200)
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 <patrik.buhr@est.tech>
Issue-ID: NONRTRIC-810

22 files changed:
api/ics-api.json
api/ics-api.yaml
src/main/java/org/oransc/ics/BeanFactory.java
src/main/java/org/oransc/ics/controllers/a1e/A1eConsts.java
src/main/java/org/oransc/ics/controllers/a1e/A1eController.java
src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerConsts.java
src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerController.java
src/main/java/org/oransc/ics/controllers/r1producer/ProducerCallbacks.java
src/main/java/org/oransc/ics/controllers/r1producer/ProducerConsts.java
src/main/java/org/oransc/ics/controllers/r1producer/ProducerController.java
src/main/java/org/oransc/ics/controllers/r1producer/ProducerJobInfo.java
src/main/java/org/oransc/ics/repository/InfoJob.java
src/main/java/org/oransc/ics/repository/InfoJobs.java
src/main/java/org/oransc/ics/repository/InfoProducer.java
src/main/java/org/oransc/ics/repository/InfoProducerRegistrationInfo.java [new file with mode: 0644]
src/main/java/org/oransc/ics/repository/InfoProducers.java
src/main/java/org/oransc/ics/repository/InfoType.java
src/main/java/org/oransc/ics/repository/InfoTypeSubscriptions.java
src/main/java/org/oransc/ics/repository/InfoTypes.java
src/main/java/org/oransc/ics/repository/MultiMap.java
src/test/java/org/oransc/ics/ApplicationTest.java
src/test/java/org/oransc/ics/controller/ProducerSimulatorController.java

index 3dece23..d3b72cc 100644 (file)
             },
             "delete": {
                 "summary": "Individual Information Type",
+                "description": "Existing jobs of the type will be automatically deleted.",
                 "operationId": "deleteInfoType",
                 "responses": {
                     "200": {
         }},
         "/A1-EI/v1/eijobs/{eiJobId}/status": {"get": {
             "summary": "EI job status",
-            "operationId": "getEiJobStatus_1",
+            "operationId": "getEiJobStatus",
             "responses": {
                 "200": {
                     "description": "EI job status",
         }},
         "/data-consumer/v1/info-jobs/{infoJobId}/status": {"get": {
             "summary": "Job status",
-            "operationId": "getEiJobStatus",
+            "operationId": "getInfoJobStatus",
             "responses": {
                 "200": {
                     "description": "Information subscription job status",
         "/A1-EI/v1/eijobs/{eiJobId}": {
             "get": {
                 "summary": "Individual EI job",
-                "operationId": "getIndividualEiJob_1",
+                "operationId": "getIndividualEiJob",
                 "responses": {
                     "200": {
                         "description": "EI job",
             },
             "delete": {
                 "summary": "Individual EI job",
-                "operationId": "deleteIndividualEiJob_1",
+                "operationId": "deleteIndividualEiJob",
                 "responses": {
                     "200": {
                         "description": "Not used",
                     "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": {
         "/data-consumer/v1/info-jobs/{infoJobId}": {
             "get": {
                 "summary": "Individual data subscription job",
-                "operationId": "getIndividualEiJob",
+                "operationId": "getIndividualInfoJob",
                 "responses": {
                     "200": {
                         "description": "Information subscription job",
             },
             "delete": {
                 "summary": "Individual data subscription job",
-                "operationId": "deleteIndividualEiJob",
+                "operationId": "deleteIndividualInfoJob",
                 "responses": {
                     "200": {
                         "description": "Not used",
                     "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": {
                 "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)"]
index 653f529..ab9ca1f 100644 (file)
@@ -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
index 3847cc3..ac6190f 100644 (file)
@@ -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) {
index 03d8ce2..5ee7715 100644 (file)
@@ -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() {
     }
 }
index 0c2aca6..9665576 100644 (file)
@@ -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<Object> getEiTypeIdentifiers( //
     ) {
         List<String> 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<Object> 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<String> 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<Object> 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<Object> 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<Object> 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<ResponseEntity<Object>> 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<InfoJob> startEiJob(InfoJob newEiJob) {
-        return this.producerCallbacks.startInfoSubscriptionJob(newEiJob, infoProducers) //
+    private Mono<InfoJob> 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<InfoJob> validatePutEiJob(String eiJobId, A1eEiJobInfo eiJobInfo) {
+    private Mono<InfoJob> 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());
     }
 }
index 00ab0f2..f53c0c5 100644 (file)
@@ -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 =
index e8b6dff..7de9813 100644 (file)
@@ -166,7 +166,7 @@ public class ConsumerController {
             List<String> 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<Object> getIndividualEiJob( //
+    public ResponseEntity<Object> 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<Object> getEiJobStatus( //
+    public ResponseEntity<Object> 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<String> 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<Object> deleteIndividualEiJob( //
+    public ResponseEntity<Object> 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<ResponseEntity<Object>> 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<InfoJob> startInfoSubscriptionJob(InfoJob newInfoJob) {
-        return this.producerCallbacks.startInfoSubscriptionJob(newInfoJob, infoProducers) //
+    private Mono<InfoJob> 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<InfoJob> validatePutInfoJob(String jobId, ConsumerJobInfo jobInfo) {
+    private Mono<InfoJob> 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());
     }
 }
index 0881cd7..2cb598e 100644 (file)
@@ -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<Integer> startInfoSubscriptionJob(InfoJob infoJob, InfoProducers infoProducers) {
+    public Mono<Integer> 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<InfoProducer> getProducersForJob(InfoJob infoJob, InfoProducers infoProducers) {
-        return infoProducers.getProducersForType(infoJob.getTypeId());
+    private Collection<InfoProducer> getProducersForJob(InfoType type, InfoProducers infoProducers) {
+        return infoProducers.getProducersSupportingType(type);
     }
 
 }
index e3f667a..bc94c1e 100644 (file)
@@ -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() {
     }
 
index 5241001..15ea11a 100644 (file)
@@ -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<String> 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<InfoProducer> 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<InfoType> supportedTypes = new ArrayList<>();
+        Set<InfoType> 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) //
index 1dd2afb..bbaa438 100644 (file)
@@ -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() {
index ffecdc9..87187b2 100644 (file)
@@ -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);
+    }
+
 }
index 1654e1f..8f8e0e9 100644 (file)
@@ -52,21 +52,23 @@ import org.springframework.util.FileSystemUtils;
 public class InfoJobs {
     private Map<String, InfoJob> allEiJobs = new HashMap<>();
 
-    private MultiMap<InfoJob> jobsByType = new MultiMap<>();
-    private MultiMap<InfoJob> jobsByOwner = new MultiMap<>();
+    private MultiMap<String, InfoJob> jobsByType = new MultiMap<>();
+    private MultiMap<String, InfoJob> 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());
index 01ec263..3fa278e 100644 (file)
@@ -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<InfoType> infoTypes;
+    private final Set<InfoType> infoTypes;
 
     @Getter
     private final String jobCallbackUrl;
@@ -43,7 +42,7 @@ public class InfoProducer {
 
     private int unresponsiveCounter = 0;
 
-    public InfoProducer(String id, Collection<InfoType> infoTypes, String jobCallbackUrl,
+    public InfoProducer(String id, Set<InfoType> 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 (file)
index 0000000..7aafdfa
--- /dev/null
@@ -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<InfoType> supportedTypes;
+
+    String jobCallbackUrl;
+
+    String producerSupervisionCallbackUrl;
+}
index 19b2698..4ea881f 100644 (file)
@@ -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<String, InfoProducer> allEiProducers = new HashMap<>();
-    private final MultiMap<InfoProducer> producersByType = new MultiMap<>();
+    private final Map<String, InfoProducer> 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<InfoType> 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<InfoType> previousTypes =
             previousDefinition != null ? previousDefinition.getInfoTypes() : new ArrayList<>();
@@ -105,11 +83,11 @@ public class InfoProducers {
     }
 
     public synchronized Collection<InfoProducer> 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<InfoProducer> getProducersForType(InfoType type) {
-        return this.producersByType.get(type.getId());
     }
 
-    public synchronized Collection<InfoProducer> getProducersForType(String typeId) {
-        return this.producersByType.get(typeId);
+    public synchronized Collection<InfoProducer> getProducersSupportingType(InfoType type) {
+        InfoType.TypeId id = type.getTypeId();
+        Collection<InfoProducer> 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<String> getProducerIdsForType(String typeId) {
+    public synchronized Collection<String> getProducerIdsForType(InfoType type) {
         Collection<String> 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;
     }
index 1a3f6e5..db7e26b 100644 (file)
 
 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<InfoType> getCompatibleTypes(Collection<InfoType> allTypes, TypeId requestedTypeId) {
+        List<InfoType> 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);
+    }
+
 }
index a72a259..2557f3c 100644 (file)
@@ -62,7 +62,7 @@ import reactor.util.retry.Retry;
 public class InfoTypeSubscriptions {
     private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
     private final Map<String, SubscriptionInfo> allSubscriptions = new HashMap<>();
-    private final MultiMap<SubscriptionInfo> subscriptionsByOwner = new MultiMap<>();
+    private final MultiMap<String, SubscriptionInfo> subscriptionsByOwner = new MultiMap<>();
     private final Gson gson = new GsonBuilder().create();
     private final ApplicationConfig config;
     private final Map<String, ConsumerCallbackHandler> 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) {
index 9d2abc2..110bca2 100644 (file)
@@ -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<String, InfoType> allEiTypes = new HashMap<>();
+    private final Map<String, InfoType> 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<InfoType> 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<InfoType> 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());
index 0f3be0a..0ba57b7 100644 (file)
 
 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<T> {
+public class MultiMap<K, V> {
 
-    private final Map<String, Map<String, T>> map = new HashMap<>();
+    private final Map<K, Set<V>> 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<String, T> innerMap = this.map.get(key);
+    public synchronized void remove(String key, V id) {
+        Set<V> 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<T> get(String key) {
-        Map<String, T> innerMap = this.map.get(key);
+    public synchronized Collection<V> get(K key) {
+        Set<V> innerMap = this.map.get(key);
         if (innerMap == null) {
             return Collections.emptyList();
         }
-        return new Vector<>(innerMap.values());
+        Collection<V> result = new ArrayList<>(innerMap.size());
+        result.addAll(innerMap);
+        return result;
     }
 
-    public void clear() {
+    public synchronized void clear() {
         this.map.clear();
     }
 
index 73a79ee..77382b7 100644 (file)
@@ -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<String> 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<String> 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<String> 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);
index a5ddc49..af219f6 100644 (file)
@@ -207,9 +207,9 @@ public class ProducerSimulatorController {
     }
 
     private void logHeaders(Map<String, String> 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");
     }
 
 }