d43fa8bf426bb9660600e7c8de6e4b774ebac7f8
[aai/search-data-service.git] / src / main / java / org / onap / aai / sa / rest / IndexApi.java
1 /**
2  * ============LICENSE_START=======================================================
3  * org.onap.aai
4  * ================================================================================
5  * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved.
6  * Copyright © 2017-2018 Amdocs
7  * ================================================================================
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *       http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  * ============LICENSE_END=========================================================
20  */
21 package org.onap.aai.sa.rest;
22
23 import com.fasterxml.jackson.databind.ObjectMapper;
24
25 import org.onap.aai.sa.searchdbabstraction.elasticsearch.dao.DocumentStoreInterface;
26 import org.onap.aai.sa.searchdbabstraction.elasticsearch.exception.DocumentStoreOperationException;
27 import org.onap.aai.sa.searchdbabstraction.entity.OperationResult;
28 import org.onap.aai.sa.searchdbabstraction.logging.SearchDbMsgs;
29 import org.onap.aai.cl.api.LogFields;
30 import org.onap.aai.cl.api.LogLine;
31 import org.onap.aai.cl.api.Logger;
32 import org.onap.aai.cl.eelf.LoggerFactory;
33 import org.onap.aai.sa.rest.DocumentFieldSchema;
34 import org.onap.aai.sa.rest.DocumentSchema;
35 import org.slf4j.MDC;
36
37 import java.io.FileNotFoundException;
38 import java.io.IOException;
39 import javax.servlet.http.HttpServletRequest;
40
41 // Spring Imports
42 import org.springframework.http.HttpHeaders;
43 import org.springframework.http.MediaType;
44 import org.springframework.http.ResponseEntity;
45 import org.springframework.http.HttpStatus;
46 // import org.springframework.http.server.HttpServletRequest;
47
48
49 /**
50  * This class encapsulates the REST end points associated with manipulating
51  * indexes in the document store.
52  */
53 public class IndexApi {
54
55
56   private static final String HEADER_VALIDATION_SUCCESS = "SUCCESS";
57   protected SearchServiceApi searchService = null;
58
59   /**
60    * Configuration for the custom analyzers that will be used for indexing.
61    */
62   protected AnalysisConfiguration analysisConfig;
63
64   // Set up the loggers.
65   private static Logger logger = LoggerFactory.getInstance()
66     .getLogger(IndexApi.class.getName());
67   private static Logger auditLogger = LoggerFactory.getInstance()
68     .getAuditLogger(IndexApi.class.getName());
69
70
71   public IndexApi(SearchServiceApi searchService) {
72     this.searchService = searchService;
73     init();
74   }
75
76
77   /**
78    * Initializes the end point.
79    *
80    * @throws FileNotFoundException
81    * @throws IOException
82    * @throws DocumentStoreOperationException
83    */
84   public void init() {
85
86     // Instantiate our analysis configuration object.
87     analysisConfig = new AnalysisConfiguration();
88   }
89
90
91   /**
92    * Processes client requests to create a new index and document type in the
93    * document store.
94    *
95    * @param documentSchema - The contents of the request body which is expected
96    *                       to be a JSON structure which corresponds to the
97    *                       schema defined in document.schema.json
98    * @param index          - The name of the index to create.
99    * @return - A Standard REST response
100    */
101   public ResponseEntity<String> processCreateIndex (String documentSchema,
102                                                     HttpServletRequest request,
103                                                     HttpHeaders headers,
104                                                     String index,
105                                                     DocumentStoreInterface documentStore) {
106
107     int resultCode = 500;
108     String resultString = "Unexpected error";
109    
110     // Initialize the MDC Context for logging purposes.
111     ApiUtils.initMdcContext(request, headers);
112
113     // Validate that the request is correctly authenticated before going
114     // any further.
115     try {
116
117       if (!searchService.validateRequest(headers, request,
118                                          ApiUtils.Action.POST, ApiUtils.SEARCH_AUTH_POLICY_NAME)) {
119         logger.warn(SearchDbMsgs.INDEX_CREATE_FAILURE, index, "Authentication failure.");
120         return errorResponse(HttpStatus.FORBIDDEN, "Authentication failure.", request);
121       }
122
123     } catch (Exception e) {
124
125       logger.warn(SearchDbMsgs.INDEX_CREATE_FAILURE, index,
126                   "Unexpected authentication failure - cause: " + e.getMessage());
127       return errorResponse(HttpStatus.FORBIDDEN, "Authentication failure.", request);
128     }
129
130
131     // We expect a payload containing the document schema.  Make sure
132     // it is present.
133     if (documentSchema == null) {
134       logger.warn(SearchDbMsgs.INDEX_CREATE_FAILURE, index, "Missing document schema payload");
135       return errorResponse(HttpStatus.valueOf(resultCode), "Missing payload", request);
136     }
137
138     try {
139
140       // Marshal the supplied json string into a document schema object.
141       ObjectMapper mapper = new ObjectMapper();
142       DocumentSchema schema = mapper.readValue(documentSchema, DocumentSchema.class);
143
144       // Now, ask the DAO to create the index.
145       OperationResult result = documentStore.createIndex(index, schema);
146
147       // Extract the result code and string from the OperationResult
148       // object so that we can use them to generate a standard REST
149       // response.
150       // Note that we want to return a 201 result code on a successful
151       // create, so if we get back a 200 from the document store,
152       // translate that int a 201.
153       resultCode = (result.getResultCode() == 200) ? 201 : result.getResultCode();
154       resultString = (result.getFailureCause() == null)
155         ? result.getResult() : result.getFailureCause();
156
157     } catch (com.fasterxml.jackson.core.JsonParseException
158              | com.fasterxml.jackson.databind.JsonMappingException e) {
159
160       // We were unable to marshal the supplied json string into a valid
161       // document schema, so return an appropriate error response.
162       resultCode = HttpStatus.BAD_REQUEST.value();
163       resultString = "Malformed schema: " + e.getMessage();
164
165     } catch (IOException e) {
166
167       // We'll treat this is a general internal error.
168       resultCode = HttpStatus.INTERNAL_SERVER_ERROR.value();
169       resultString = "IO Failure: " + e.getMessage();
170     }
171
172     ResponseEntity<String> response = ResponseEntity.status(resultCode).contentType ( MediaType.APPLICATION_JSON ).body(resultString);
173
174
175     // Log the result.
176     if ((response.getStatusCodeValue() >= 200) && (response.getStatusCodeValue() < 300)) {
177       logger.info(SearchDbMsgs.CREATED_INDEX, index);
178     } else {
179       logger.warn(SearchDbMsgs.INDEX_CREATE_FAILURE, index, resultString);
180     }
181
182     // Generate our audit log.
183     auditLogger.info(SearchDbMsgs.PROCESS_REST_REQUEST,
184                      new LogFields()
185                      .setField(LogLine.DefinedFields.RESPONSE_CODE, resultCode)
186                      .setField(LogLine.DefinedFields.RESPONSE_DESCRIPTION,
187                                HttpStatus.valueOf(resultCode).toString()),
188                      (request != null) ? request.getMethod().toString () : "Unknown",
189                      (request != null) ? request.getRequestURL ().toString () : "Unknown",
190                      (request != null) ? request.getRemoteHost () : "Unknown",
191                      Integer.toString(response.getStatusCodeValue ()));
192
193
194
195     // Clear the MDC context so that no other transaction inadvertently
196     // uses our transaction id.
197     ApiUtils.clearMdcContext();
198
199     // Finally, return the response.
200     return response;
201   }
202
203   /**
204    * This function accepts any JSON and will "blindly" write it to the
205    * document store.
206    *
207    * Note, eventually this "dynamic" flow should follow the same JSON-Schema
208    * validation procedure as the normal create index flow.
209    *
210    * @param dynamicSchema - The JSON string that will be sent to the document store.
211    * @param index - The name of the index to be created.
212    * @param documentStore - The document store specific interface.
213    * @return The result of the document store interface's operation.
214    */
215   public ResponseEntity<String> processCreateDynamicIndex(String dynamicSchema, HttpServletRequest request,
216                                             HttpHeaders headers, String index, DocumentStoreInterface documentStore) {
217
218     ResponseEntity<String> response = null;
219
220     ResponseEntity<String> validationResponse = validateRequest(request, headers, index, SearchDbMsgs.INDEX_CREATE_FAILURE);
221
222
223     if (validationResponse.getStatusCodeValue () != HttpStatus.OK.value ()) {
224       response = validationResponse;
225     } else {
226       OperationResult result = documentStore.createDynamicIndex(index, dynamicSchema);
227
228       int resultCode = (result.getResultCode() == 200) ? 201 : result.getResultCode();
229       String resultString = (result.getFailureCause() == null) ? result.getResult() : result.getFailureCause();
230
231       response = ResponseEntity.status(resultCode).body(resultString);
232     }
233
234     return response;
235   }
236
237   /**
238    * Processes a client request to remove an index from the document store.
239    * Note that this implicitly deletes all documents contained within that index.
240    *
241    * @param index - The index to be deleted.
242    * @return - A standard REST response.
243    */
244   public ResponseEntity<String> processDelete(String index,
245                                               HttpServletRequest request,
246                                               HttpHeaders headers,
247                                               DocumentStoreInterface documentStore) {
248
249     // Initialize the MDC Context for logging purposes.
250     ApiUtils.initMdcContext(request, headers);
251
252     // Set a default response in case something unexpected goes wrong.
253     ResponseEntity<String> response = ResponseEntity.status ( HttpStatus.INTERNAL_SERVER_ERROR ).body ( "Unknown" );
254
255     // Validate that the request is correctly authenticated before going
256     // any further.
257     try {
258
259       if (!searchService.validateRequest(headers, request, ApiUtils.Action.POST,
260                                          ApiUtils.SEARCH_AUTH_POLICY_NAME)) {
261         logger.warn(SearchDbMsgs.INDEX_CREATE_FAILURE, index, "Authentication failure.");
262         return errorResponse(HttpStatus.FORBIDDEN, "Authentication failure.", request);
263       }
264
265     } catch (Exception e) {
266
267       logger.warn(SearchDbMsgs.INDEX_CREATE_FAILURE, index,
268                   "Unexpected authentication failure - cause: " + e.getMessage());
269       return errorResponse(HttpStatus.FORBIDDEN, "Authentication failure.", request);
270     }
271
272
273     try {
274       // Send the request to the document store.
275       response = responseFromOperationResult(documentStore.deleteIndex(index));
276
277     } catch (DocumentStoreOperationException e) {
278       response = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).contentType ( MediaType.APPLICATION_JSON ).body(e.getMessage());
279     }
280
281     // Log the result.
282     if ((response.getStatusCodeValue() >= 200) && (response.getStatusCodeValue() < 300)) {
283       logger.info(SearchDbMsgs.DELETED_INDEX, index);
284     } else {
285       logger.warn(SearchDbMsgs.INDEX_DELETE_FAILURE, index, (String) response.getBody ());
286     }
287
288     // Generate our audit log.
289     auditLogger.info(SearchDbMsgs.PROCESS_REST_REQUEST,
290                      new LogFields()
291                      .setField(LogLine.DefinedFields.RESPONSE_CODE, response.getStatusCodeValue())
292                      .setField(LogLine.DefinedFields.RESPONSE_DESCRIPTION,
293                                response.getStatusCode ().getReasonPhrase()),
294                      (request != null) ? request.getMethod().toString () : "Unknown",
295                      (request != null) ? request.getRequestURL ().toString () : "Unknown",
296                      (request != null) ? request.getRemoteHost () : "Unknown",
297                      Integer.toString(response.getStatusCodeValue()));
298
299     // Clear the MDC context so that no other transaction inadvertently
300     // uses our transaction id.
301     ApiUtils.clearMdcContext();
302
303     return response;
304   }
305
306
307   /**
308    * This method takes a JSON format document schema and produces a set of
309    * field mappings in the form that Elastic Search expects.
310    *
311    * @param documentSchema - A document schema expressed as a JSON string.
312    * @return - A JSON string expressing an Elastic Search mapping configuration.
313    * @throws com.fasterxml.jackson.core.JsonParseException
314    * @throws com.fasterxml.jackson.databind.JsonMappingException
315    * @throws IOException
316    */
317   public String generateDocumentMappings(String documentSchema)
318     throws com.fasterxml.jackson.core.JsonParseException,
319            com.fasterxml.jackson.databind.JsonMappingException, IOException {
320
321     // Unmarshal the json content into a document schema object.
322     ObjectMapper mapper = new ObjectMapper();
323     DocumentSchema schema = mapper.readValue(documentSchema, DocumentSchema.class);
324
325     // Now, generate the Elastic Search mapping json and return it.
326     StringBuilder sb = new StringBuilder();
327     sb.append("{");
328     sb.append("\"properties\": {");
329
330     boolean first = true;
331     for (DocumentFieldSchema field : schema.getFields()) {
332
333       if (!first) {
334         sb.append(",");
335       } else {
336         first = false;
337       }
338
339       sb.append("\"").append(field.getName()).append("\": {");
340
341       // The field type is mandatory.
342       sb.append("\"type\": \"").append(field.getDataType()).append("\"");
343
344       // If the index field was specified, then append it.
345       if (field.getSearchable() != null) {
346         sb.append(", \"index\": \"").append(field.getSearchable()
347                                             ? "analyzed" : "not_analyzed").append("\"");
348       }
349
350       // If a search analyzer was specified, then append it.
351       if (field.getSearchAnalyzer() != null) {
352         sb.append(", \"search_analyzer\": \"").append(field.getSearchAnalyzer()).append("\"");
353       }
354
355       // If an indexing analyzer was specified, then append it.
356       if (field.getIndexAnalyzer() != null) {
357         sb.append(", \"analyzer\": \"").append(field.getIndexAnalyzer()).append("\"");
358       } else {
359         sb.append(", \"analyzer\": \"").append("whitespace").append("\"");
360       }
361
362       sb.append("}");
363     }
364
365     sb.append("}");
366     sb.append("}");
367
368     logger.debug("Generated document mappings: " + sb.toString());
369
370     return sb.toString();
371   }
372
373
374   /**
375    * Converts an {@link OperationResult} to a standard REST {@link ResponseEntity}
376    * object.
377    *
378    * @param result - The {@link OperationResult} to be converted.
379    * @return - The equivalent {@link ResponseEntity} object.
380    */
381   public ResponseEntity<String> responseFromOperationResult(OperationResult result) {
382
383     if ((result.getResultCode() >= 200) && (result.getResultCode() < 300)) {
384       return ResponseEntity.status(result.getResultCode()).contentType ( MediaType.APPLICATION_JSON ).body(result.getResult());
385     } else {
386       if (result.getFailureCause() != null) {
387         return ResponseEntity.status(result.getResultCode()).contentType ( MediaType.APPLICATION_JSON ).body(result.getFailureCause());
388       } else {
389         return ResponseEntity.status(result.getResultCode()).contentType ( MediaType.APPLICATION_JSON ).body(result.getResult());
390       }
391     }
392   }
393
394   public ResponseEntity<String> errorResponse(HttpStatus status, String msg, HttpServletRequest request) {
395
396     // Generate our audit log.
397     auditLogger.info(SearchDbMsgs.PROCESS_REST_REQUEST,
398                      new LogFields()
399                      .setField(LogLine.DefinedFields.RESPONSE_CODE, status.value ())
400                      .setField(LogLine.DefinedFields.RESPONSE_DESCRIPTION, status.getReasonPhrase()),
401                      (request != null) ? request.getMethod().toString () : "Unknown",
402                      (request != null) ? request.getRequestURL ().toString () : "Unknown",
403                      (request != null) ? request.getRemoteHost () : "Unknown",
404                      Integer.toString(status.value ()));
405
406     // Clear the MDC context so that no other transaction inadvertently
407     // uses our transaction id.
408     ApiUtils.clearMdcContext();
409
410     return ResponseEntity.status(status).contentType ( MediaType.APPLICATION_JSON ).body(msg);
411   }
412
413
414   /**
415    * A helper method used for validating/authenticating an incoming request.
416    *
417    * @param request - The http request that will be validated.
418    * @param headers - The http headers that will be validated.
419    * @param index - The name of the index that the document store request is being made against.
420    * @param failureMsgEnum - The logging message to be used upon validation failure.
421    * @return A success or failure response
422    */
423   private ResponseEntity<String> validateRequest(HttpServletRequest request, HttpHeaders headers, String index, SearchDbMsgs failureMsgEnum) {
424     try {
425       if (!searchService.validateRequest(headers, request, ApiUtils.Action.POST, ApiUtils.SEARCH_AUTH_POLICY_NAME)) {
426         logger.warn(failureMsgEnum, index, "Authentication failure.");
427         return errorResponse(HttpStatus.FORBIDDEN, "Authentication failure.", request);
428       }
429     } catch (Exception e) {
430       logger.warn(failureMsgEnum, index, "Unexpected authentication failure - cause: " + e.getMessage());
431       return errorResponse(HttpStatus.FORBIDDEN, "Authentication failure.", request);
432     }
433     return ResponseEntity.status(HttpStatus.OK).body(HEADER_VALIDATION_SUCCESS);
434   }
435 }