Added supervision of producers
[nonrtric.git] / enrichment-coordinator-service / src / main / java / org / oransc / enrichment / controllers / consumer / ConsumerController.java
1 /*-
2  * ========================LICENSE_START=================================
3  * ONAP : ccsdk oran
4  * ======================================================================
5  * Copyright (C) 2019-2020 Nordix Foundation. All rights reserved.
6  * ======================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ========================LICENSE_END===================================
19  */
20
21 package org.oransc.enrichment.controllers.consumer;
22
23 import com.fasterxml.jackson.databind.ObjectMapper;
24 import com.google.gson.Gson;
25 import com.google.gson.GsonBuilder;
26
27 import io.swagger.annotations.Api;
28 import io.swagger.annotations.ApiOperation;
29 import io.swagger.annotations.ApiParam;
30 import io.swagger.annotations.ApiResponse;
31 import io.swagger.annotations.ApiResponses;
32
33 import java.util.ArrayList;
34 import java.util.List;
35
36 import org.everit.json.schema.Schema;
37 import org.everit.json.schema.loader.SchemaLoader;
38 import org.json.JSONObject;
39 import org.oransc.enrichment.clients.ProducerCallbacks;
40 import org.oransc.enrichment.configuration.ApplicationConfig;
41 import org.oransc.enrichment.controllers.ErrorResponse;
42 import org.oransc.enrichment.controllers.VoidResponse;
43 import org.oransc.enrichment.exceptions.ServiceException;
44 import org.oransc.enrichment.repository.EiJob;
45 import org.oransc.enrichment.repository.EiJobs;
46 import org.oransc.enrichment.repository.EiType;
47 import org.oransc.enrichment.repository.EiTypes;
48 import org.oransc.enrichment.repository.ImmutableEiJob;
49 import org.springframework.beans.factory.annotation.Autowired;
50 import org.springframework.http.HttpStatus;
51 import org.springframework.http.MediaType;
52 import org.springframework.http.ResponseEntity;
53 import org.springframework.web.bind.annotation.DeleteMapping;
54 import org.springframework.web.bind.annotation.GetMapping;
55 import org.springframework.web.bind.annotation.PathVariable;
56 import org.springframework.web.bind.annotation.PutMapping;
57 import org.springframework.web.bind.annotation.RequestBody;
58 import org.springframework.web.bind.annotation.RestController;
59 import reactor.core.publisher.Mono;
60
61 @SuppressWarnings("java:S3457") // No need to call "toString()" method as formatting and string ..
62 @RestController("ConsumerController")
63 @Api(tags = {ConsumerConsts.CONSUMER_API_NAME})
64 public class ConsumerController {
65
66     @Autowired
67     ApplicationConfig applicationConfig;
68
69     @Autowired
70     private EiJobs eiJobs;
71
72     @Autowired
73     private EiTypes eiTypes;
74
75     @Autowired
76     ProducerCallbacks producerCallbacks;
77
78     private static Gson gson = new GsonBuilder() //
79         .serializeNulls() //
80         .create(); //
81
82     @GetMapping(path = ConsumerConsts.API_ROOT + "/eitypes", produces = MediaType.APPLICATION_JSON_VALUE)
83     @ApiOperation(value = "EI type identifiers", notes = "")
84     @ApiResponses(
85         value = { //
86             @ApiResponse(
87                 code = 200,
88                 message = "EI type identifiers",
89                 response = String.class,
90                 responseContainer = "List"), //
91         })
92     public ResponseEntity<Object> getEiTypeIdentifiers( //
93     ) {
94         List<String> result = new ArrayList<>();
95         for (EiType eiType : this.eiTypes.getAllEiTypes()) {
96             result.add(eiType.getId());
97         }
98
99         return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
100     }
101
102     @GetMapping(path = ConsumerConsts.API_ROOT + "/eitypes/{eiTypeId}", produces = MediaType.APPLICATION_JSON_VALUE)
103     @ApiOperation(value = "Individual EI type", notes = "")
104     @ApiResponses(
105         value = { //
106             @ApiResponse(code = 200, message = "EI type", response = ConsumerEiTypeInfo.class), //
107             @ApiResponse(
108                 code = 404,
109                 message = "Enrichment Information type is not found",
110                 response = ErrorResponse.ErrorInfo.class)})
111     public ResponseEntity<Object> getEiType( //
112         @PathVariable("eiTypeId") String eiTypeId) {
113         try {
114             EiType t = this.eiTypes.getType(eiTypeId);
115             ConsumerEiTypeInfo info = toEiTypeInfo(t);
116             return new ResponseEntity<>(gson.toJson(info), HttpStatus.OK);
117         } catch (Exception e) {
118             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
119         }
120     }
121
122     @GetMapping(
123         path = ConsumerConsts.API_ROOT + "/eitypes/{eiTypeId}/eijobs",
124         produces = MediaType.APPLICATION_JSON_VALUE)
125     @ApiOperation(value = "EI job identifiers", notes = "")
126     @ApiResponses(
127         value = { //
128             @ApiResponse(
129                 code = 200,
130                 message = "EI job identifiers",
131                 response = String.class,
132                 responseContainer = "List"), //
133             @ApiResponse(
134                 code = 404,
135                 message = "Enrichment Information type is not found",
136                 response = ErrorResponse.ErrorInfo.class)})
137     public ResponseEntity<Object> getEiJobIds( //
138         @PathVariable("eiTypeId") String eiTypeId, //
139         @ApiParam(
140             name = ConsumerConsts.OWNER_PARAM,
141             required = false, //
142             value = ConsumerConsts.OWNER_PARAM_DESCRIPTION) //
143         String owner) {
144         try {
145             this.eiTypes.getType(eiTypeId); // Just to check that the type exists
146             List<String> result = new ArrayList<>();
147             for (EiJob job : this.eiJobs.getJobsForType(eiTypeId)) {
148                 result.add(job.id());
149             }
150             return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
151         } catch (Exception e) {
152             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
153         }
154     }
155
156     @GetMapping(
157         path = ConsumerConsts.API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}",
158         produces = MediaType.APPLICATION_JSON_VALUE)
159     @ApiOperation(value = "Individual EI Job", notes = "")
160     @ApiResponses(
161         value = { //
162             @ApiResponse(code = 200, message = "EI Job", response = ConsumerEiJobInfo.class), //
163             @ApiResponse(
164                 code = 404,
165                 message = "Enrichment Information type or job is not found",
166                 response = ErrorResponse.ErrorInfo.class)})
167     public ResponseEntity<Object> getIndividualEiJob( //
168         @PathVariable("eiTypeId") String eiTypeId, //
169         @PathVariable("eiJobId") String eiJobId) {
170         try {
171             this.eiTypes.getType(eiTypeId); // Just to check that the type exists
172             EiJob job = this.eiJobs.getJob(eiJobId);
173             return new ResponseEntity<>(gson.toJson(toEiJobInfo(job)), HttpStatus.OK);
174         } catch (Exception e) {
175             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
176         }
177     }
178
179     @GetMapping(
180         path = ConsumerConsts.API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}/status",
181         produces = MediaType.APPLICATION_JSON_VALUE)
182     @ApiOperation(value = "EI Job status", notes = "")
183     @ApiResponses(
184         value = { //
185             @ApiResponse(code = 200, message = "EI Job status", response = ConsumerEiJobStatus.class), //
186             @ApiResponse(
187                 code = 404,
188                 message = "Enrichment Information type or job is not found",
189                 response = ErrorResponse.ErrorInfo.class)})
190     public ResponseEntity<Object> getEiJobStatus( //
191         @PathVariable("eiTypeId") String eiTypeId, //
192         @PathVariable("eiJobId") String eiJobId) {
193         try {
194             this.eiTypes.getType(eiTypeId); // Just to check that the type exists
195             EiJob job = this.eiJobs.getJob(eiJobId);
196             return new ResponseEntity<>(gson.toJson(toEiJobStatus(job)), HttpStatus.OK);
197         } catch (Exception e) {
198             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
199         }
200     }
201
202     private ConsumerEiJobStatus toEiJobStatus(EiJob job) {
203         // TODO
204         return new ConsumerEiJobStatus(ConsumerEiJobStatus.OperationalState.ENABLED);
205     }
206
207     @DeleteMapping(
208         path = ConsumerConsts.API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}",
209         produces = MediaType.APPLICATION_JSON_VALUE)
210     @ApiOperation(value = "Individual EI Job", notes = "")
211     @ApiResponses(
212         value = { //
213             @ApiResponse(code = 200, message = "Not used", response = VoidResponse.class),
214             @ApiResponse(code = 204, message = "Job deleted", response = VoidResponse.class),
215             @ApiResponse(
216                 code = 404,
217                 message = "Enrichment Information type or job is not found",
218                 response = ErrorResponse.ErrorInfo.class)})
219     public ResponseEntity<Object> deleteIndividualEiJob( //
220         @PathVariable("eiTypeId") String eiTypeId, //
221         @PathVariable("eiJobId") String eiJobId) {
222         try {
223             EiJob job = this.eiJobs.getJob(eiJobId);
224             this.eiJobs.remove(job);
225             this.producerCallbacks.notifyProducersJobDeleted(job);
226             return new ResponseEntity<>(HttpStatus.NO_CONTENT);
227         } catch (Exception e) {
228             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
229         }
230     }
231
232     @PutMapping(
233         path = ConsumerConsts.API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}", //
234         produces = MediaType.APPLICATION_JSON_VALUE, //
235         consumes = MediaType.APPLICATION_JSON_VALUE)
236     @ApiOperation(value = "Individual EI Job", notes = "")
237     @ApiResponses(
238         value = { //
239             @ApiResponse(code = 201, message = "Job created", response = VoidResponse.class), //
240             @ApiResponse(code = 200, message = "Job updated", response = VoidResponse.class), // ,
241             @ApiResponse(
242                 code = 404,
243                 message = "Enrichment Information type is not found",
244                 response = ErrorResponse.ErrorInfo.class)})
245     public Mono<ResponseEntity<Object>> putIndividualEiJob( //
246         @PathVariable("eiTypeId") String eiTypeId, //
247         @PathVariable("eiJobId") String eiJobId, //
248         @RequestBody ConsumerEiJobInfo eiJobInfo) {
249
250         final boolean isNewJob = this.eiJobs.get(eiJobId) == null;
251
252         return validatePutEiJob(eiTypeId, eiJobId, eiJobInfo) //
253             .flatMap(this::notifyProducersNewJob) //
254             .doOnNext(newEiJob -> this.eiJobs.put(newEiJob)) //
255             .flatMap(newEiJob -> Mono.just(new ResponseEntity<>(isNewJob ? HttpStatus.CREATED : HttpStatus.OK)))
256             .onErrorResume(throwable -> Mono.just(ErrorResponse.create(throwable, HttpStatus.NOT_FOUND)));
257     }
258
259     private Mono<EiJob> notifyProducersNewJob(EiJob newEiJob) {
260         return this.producerCallbacks.notifyProducersJobStarted(newEiJob) //
261             .flatMap(noOfAcceptingProducers -> {
262                 if (noOfAcceptingProducers.intValue() > 0) {
263                     return Mono.just(newEiJob);
264                 } else {
265                     return Mono.error(new ServiceException("Job not accepted by any producers", HttpStatus.CONFLICT));
266                 }
267             });
268     }
269
270     private Mono<EiJob> validatePutEiJob(String eiTypeId, String eiJobId, ConsumerEiJobInfo eiJobInfo) {
271         try {
272             EiType eiType = this.eiTypes.getType(eiTypeId);
273             validateJsonObjectAgainstSchema(eiType.getJobDataSchema(), eiJobInfo.jobData);
274             EiJob existingEiJob = this.eiJobs.get(eiJobId);
275
276             if (existingEiJob != null && !existingEiJob.type().getId().equals(eiTypeId)) {
277                 throw new ServiceException("Not allowed to change type for existing EI job", HttpStatus.CONFLICT);
278             }
279             return Mono.just(toEiJob(eiJobInfo, eiJobId, eiType));
280         } catch (Exception e) {
281             return Mono.error(e);
282         }
283     }
284
285     private void validateJsonObjectAgainstSchema(Object schemaObj, Object object) throws ServiceException {
286         if (schemaObj != null) { // schema is optional for now
287             try {
288                 ObjectMapper mapper = new ObjectMapper();
289
290                 String schemaAsString = mapper.writeValueAsString(schemaObj);
291                 JSONObject schemaJSON = new JSONObject(schemaAsString);
292                 Schema schema = SchemaLoader.load(schemaJSON);
293
294                 String objectAsString = mapper.writeValueAsString(object);
295                 JSONObject json = new JSONObject(objectAsString);
296                 schema.validate(json);
297             } catch (Exception e) {
298                 throw new ServiceException("Json validation failure " + e.toString(), HttpStatus.CONFLICT);
299             }
300         }
301     }
302
303     // Status TBD
304
305     private EiJob toEiJob(ConsumerEiJobInfo info, String id, EiType type) {
306         return ImmutableEiJob.builder() //
307             .id(id) //
308             .type(type) //
309             .owner(info.owner) //
310             .jobData(info.jobData) //
311             .targetUri(info.targetUri) //
312             .build();
313     }
314
315     private ConsumerEiTypeInfo toEiTypeInfo(EiType t) {
316         return new ConsumerEiTypeInfo(t.getJobDataSchema());
317     }
318
319     private ConsumerEiJobInfo toEiJobInfo(EiJob s) {
320         return new ConsumerEiJobInfo(s.jobData(), s.owner(), s.targetUri());
321     }
322 }