Reject doc create requests if index does not exist
[aai/search-data-service.git] / src / main / java / org / openecomp / sa / rest / DocumentApi.java
1 /**
2  * ============LICENSE_START=======================================================
3  * Search Data Service
4  * ================================================================================
5  * Copyright © 2017 AT&T Intellectual Property.
6  * Copyright © 2017 Amdocs
7  * All rights reserved.
8  * ================================================================================
9  * Licensed under the Apache License, Version 2.0 (the "License");
10  * you may not use this file except in compliance with the License.
11  * You may obtain a copy of the License ati
12  *
13  *    http://www.apache.org/licenses/LICENSE-2.0
14  *
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  * ============LICENSE_END=========================================================
21  *
22  * ECOMP and OpenECOMP are trademarks
23  * and service marks of AT&T Intellectual Property.
24  */
25 package org.openecomp.sa.rest;
26
27 import com.fasterxml.jackson.annotation.JsonInclude.Include;
28 import com.fasterxml.jackson.core.JsonProcessingException;
29 import com.fasterxml.jackson.databind.ObjectMapper;
30 import org.openecomp.cl.api.LogFields;
31 import org.openecomp.cl.api.LogLine;
32 import org.openecomp.cl.api.Logger;
33 import org.openecomp.cl.eelf.LoggerFactory;
34 import org.openecomp.sa.searchdbabstraction.elasticsearch.dao.DocumentStoreDataEntityImpl;
35 import org.openecomp.sa.searchdbabstraction.elasticsearch.dao.DocumentStoreInterface;
36 import org.openecomp.sa.searchdbabstraction.entity.AggregationResults;
37 import org.openecomp.sa.searchdbabstraction.entity.DocumentOperationResult;
38 import org.openecomp.sa.searchdbabstraction.entity.SearchOperationResult;
39 import org.openecomp.sa.searchdbabstraction.logging.SearchDbMsgs;
40 import org.openecomp.sa.searchdbabstraction.searchapi.SearchStatement;
41
42 import javax.servlet.http.HttpServletRequest;
43 import javax.servlet.http.HttpServletResponse;
44 import javax.ws.rs.core.HttpHeaders;
45 import javax.ws.rs.core.MediaType;
46 import javax.ws.rs.core.Response;
47 import javax.ws.rs.core.Response.Status;
48
49 public class DocumentApi {
50   private static final String REQUEST_HEADER_RESOURCE_VERSION = "If-Match";
51   private static final String RESPONSE_HEADER_RESOURCE_VERSION = "ETag";
52   private static final String REQUEST_HEADER_ALLOW_IMPLICIT_INDEX_CREATION = "X-CreateIndex";
53   
54   protected SearchServiceApi searchService = null;
55
56   private Logger logger = LoggerFactory.getInstance().getLogger(DocumentApi.class.getName());
57   private Logger auditLogger = LoggerFactory.getInstance()
58       .getAuditLogger(DocumentApi.class.getName());
59
60   public DocumentApi(SearchServiceApi searchService) {
61     this.searchService = searchService;
62   }
63
64   public Response processPost(String content, HttpServletRequest request, HttpHeaders headers,
65                               HttpServletResponse httpResponse, String index,
66                               DocumentStoreInterface documentStore) {
67
68     // Initialize the MDC Context for logging purposes.
69     ApiUtils.initMdcContext(request, headers);
70
71     try {
72       ObjectMapper mapper = new ObjectMapper();
73       mapper.setSerializationInclusion(Include.NON_EMPTY);
74       if (content == null) {
75         return handleError(request, content, Status.BAD_REQUEST);
76       }
77
78       boolean isValid;
79       try {
80         isValid = searchService.validateRequest(headers, request, ApiUtils.Action.POST,
81             ApiUtils.SEARCH_AUTH_POLICY_NAME);
82       } catch (Exception e) {
83         logger.info(SearchDbMsgs.EXCEPTION_DURING_METHOD_CALL,
84             "DocumentApi.processPost",
85             e.getMessage());
86         return handleError(request, content, Status.FORBIDDEN);
87       }
88
89       if (!isValid) {
90         return handleError(request, content, Status.FORBIDDEN);
91       }
92
93       DocumentStoreDataEntityImpl document = new DocumentStoreDataEntityImpl();
94       document.setContent(content);
95
96       DocumentOperationResult result = documentStore.createDocument(index, document, implicitlyCreateIndex(headers));
97       String output = null;
98       if (result.getResultCode() >= 200 && result.getResultCode() <= 299) {
99         output = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result.getDocument());
100       } else {
101         output = result.getError() != null
102             ? mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result.getError())
103             : result.getFailureCause();
104       }
105
106       if (httpResponse != null) {
107         httpResponse.setHeader(RESPONSE_HEADER_RESOURCE_VERSION, result.getResultVersion());
108       }
109       Response response = Response.status(result.getResultCode()).entity(output).build();
110       logResult(request, Response.Status.fromStatusCode(response.getStatus()));
111
112       // Clear the MDC context so that no other transaction inadvertently
113       // uses our transaction id.
114       ApiUtils.clearMdcContext();
115
116       return response;
117     } catch (Exception e) {
118       return handleError(request, e.getMessage(), Status.INTERNAL_SERVER_ERROR);
119     }
120   }
121
122   public Response processPut(String content, HttpServletRequest request, HttpHeaders headers,
123                              HttpServletResponse httpResponse, String index,
124                              String id, DocumentStoreInterface documentStore) {
125
126     // Initialize the MDC Context for logging purposes.
127     ApiUtils.initMdcContext(request, headers);
128
129     try {
130       ObjectMapper mapper = new ObjectMapper();
131       mapper.setSerializationInclusion(Include.NON_EMPTY);
132       if (content == null) {
133         return handleError(request, content, Status.BAD_REQUEST);
134       }
135
136       boolean isValid;
137       try {
138         isValid = searchService.validateRequest(headers, request, ApiUtils.Action.PUT,
139             ApiUtils.SEARCH_AUTH_POLICY_NAME);
140       } catch (Exception e) {
141         logger.info(SearchDbMsgs.EXCEPTION_DURING_METHOD_CALL,
142             "DocumentApi.processPut",
143             e.getMessage());
144         return handleError(request, content, Status.FORBIDDEN);
145       }
146
147       if (!isValid) {
148         return handleError(request, content, Status.FORBIDDEN);
149       }
150
151       String resourceVersion = headers.getRequestHeaders()
152           .getFirst(REQUEST_HEADER_RESOURCE_VERSION);
153
154       DocumentStoreDataEntityImpl document = new DocumentStoreDataEntityImpl();
155       document.setId(id);
156       document.setContent(content);
157       document.setVersion(resourceVersion);
158
159       DocumentOperationResult result = null;
160       if (resourceVersion == null) {
161         result = documentStore.createDocument(index, document, implicitlyCreateIndex(headers));
162       } else {
163         result = documentStore.updateDocument(index, document, implicitlyCreateIndex(headers));
164       }
165
166       String output = null;
167       if (result.getResultCode() >= 200 && result.getResultCode() <= 299) {
168         output = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result.getDocument());
169       } else {
170         output = result.getError() != null
171             ? mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result.getError())
172             : result.getFailureCause();
173       }
174       if (httpResponse != null) {
175         httpResponse.setHeader(RESPONSE_HEADER_RESOURCE_VERSION, result.getResultVersion());
176       }
177       Response response = Response.status(result.getResultCode()).entity(output).build();
178       logResult(request, Response.Status.fromStatusCode(response.getStatus()));
179
180       // Clear the MDC context so that no other transaction inadvertently
181       // uses our transaction id.
182       ApiUtils.clearMdcContext();
183
184       return response;
185     } catch (Exception e) {
186       return handleError(request, e.getMessage(), Status.INTERNAL_SERVER_ERROR);
187     }
188   }
189
190   public Response processDelete(String content, HttpServletRequest request, HttpHeaders headers,
191                                 HttpServletResponse httpResponse, String index, String id,
192                                 DocumentStoreInterface documentStore) {
193
194     // Initialize the MDC Context for logging purposes.
195     ApiUtils.initMdcContext(request, headers);
196
197     try {
198       ObjectMapper mapper = new ObjectMapper();
199       mapper.setSerializationInclusion(Include.NON_EMPTY);
200       boolean isValid;
201       try {
202         isValid = searchService.validateRequest(headers, request, ApiUtils.Action.DELETE,
203             ApiUtils.SEARCH_AUTH_POLICY_NAME);
204       } catch (Exception e) {
205         logger.info(SearchDbMsgs.EXCEPTION_DURING_METHOD_CALL,
206             "DocumentApi.processDelete",
207             e.getMessage());
208         return handleError(request, content, Status.FORBIDDEN);
209       }
210
211       if (!isValid) {
212         return handleError(request, content, Status.FORBIDDEN);
213       }
214
215       String resourceVersion = headers.getRequestHeaders()
216           .getFirst(REQUEST_HEADER_RESOURCE_VERSION);
217       if (resourceVersion == null || resourceVersion.isEmpty()) {
218         return handleError(request, "Request header 'If-Match' missing",
219             javax.ws.rs.core.Response.Status.BAD_REQUEST);
220       }
221
222       DocumentStoreDataEntityImpl document = new DocumentStoreDataEntityImpl();
223       document.setId(id);
224       document.setVersion(resourceVersion);
225
226       DocumentOperationResult result = documentStore.deleteDocument(index, document);
227       String output = null;
228       if (!(result.getResultCode() >= 200 && result.getResultCode() <= 299)) { //
229         output = result.getError() != null
230             ? mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result.getError())
231             : result.getFailureCause();
232       }
233
234       if (httpResponse != null) {
235         httpResponse.setHeader(RESPONSE_HEADER_RESOURCE_VERSION, result.getResultVersion());
236       }
237       Response response;
238       if (output == null) {
239         response = Response.status(result.getResultCode()).build();
240       } else {
241         response = Response.status(result.getResultCode()).entity(output).build();
242       }
243
244       logResult(request, Response.Status.fromStatusCode(response.getStatus()));
245
246       // Clear the MDC context so that no other transaction inadvertently
247       // uses our transaction id.
248       ApiUtils.clearMdcContext();
249
250       return response;
251     } catch (Exception e) {
252       return handleError(request, e.getMessage(), Status.INTERNAL_SERVER_ERROR);
253     }
254   }
255
256   public Response processGet(String content, HttpServletRequest request, HttpHeaders headers,
257                              HttpServletResponse httpResponse, String index, String id,
258                              DocumentStoreInterface documentStore) {
259
260     // Initialize the MDC Context for logging purposes.
261     ApiUtils.initMdcContext(request, headers);
262
263     try {
264       ObjectMapper mapper = new ObjectMapper();
265       mapper.setSerializationInclusion(Include.NON_EMPTY);
266       boolean isValid;
267       try {
268         isValid = searchService.validateRequest(headers, request, ApiUtils.Action.GET,
269             ApiUtils.SEARCH_AUTH_POLICY_NAME);
270       } catch (Exception e) {
271         logger.info(SearchDbMsgs.EXCEPTION_DURING_METHOD_CALL,
272             "DocumentApi.processGet",
273             e.getMessage());
274         return handleError(request, content, Status.FORBIDDEN);
275       }
276
277       if (!isValid) {
278         return handleError(request, content, Status.FORBIDDEN);
279       }
280
281       String resourceVersion = headers.getRequestHeaders()
282           .getFirst(REQUEST_HEADER_RESOURCE_VERSION);
283
284       DocumentStoreDataEntityImpl document = new DocumentStoreDataEntityImpl();
285       document.setId(id);
286       document.setVersion(resourceVersion);
287
288       DocumentOperationResult result = documentStore.getDocument(index, document);
289       String output = null;
290       if (result.getResultCode() >= 200 && result.getResultCode() <= 299) {
291         output = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result.getDocument());
292       } else {
293         output = result.getError() != null
294             ? mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result.getError())
295             : result.getFailureCause();
296       }
297       if (httpResponse != null) {
298         httpResponse.setHeader(RESPONSE_HEADER_RESOURCE_VERSION, result.getResultVersion());
299       }
300       Response response = Response.status(result.getResultCode()).entity(output).build();
301       logResult(request, Response.Status.fromStatusCode(response.getStatus()));
302
303       // Clear the MDC context so that no other transaction inadvertently
304       // uses our transaction id.
305       ApiUtils.clearMdcContext();
306
307       return response;
308     } catch (Exception e) {
309       return handleError(request, e.getMessage(), Status.INTERNAL_SERVER_ERROR);
310     }
311   }
312
313   public Response processSearchWithGet(String content, HttpServletRequest request,
314                                        HttpHeaders headers, String index,
315                                        String queryText, DocumentStoreInterface documentStore) {
316
317     // Initialize the MDC Context for logging purposes.
318     ApiUtils.initMdcContext(request, headers);
319
320     try {
321       ObjectMapper mapper = new ObjectMapper();
322       mapper.setSerializationInclusion(Include.NON_EMPTY);
323
324       boolean isValid;
325       try {
326         isValid = searchService.validateRequest(headers, request, ApiUtils.Action.GET,
327             ApiUtils.SEARCH_AUTH_POLICY_NAME);
328       } catch (Exception e) {
329         logger.info(SearchDbMsgs.EXCEPTION_DURING_METHOD_CALL,
330             "processSearchWithGet",
331             e.getMessage());
332         return handleError(request, content, Status.FORBIDDEN);
333       }
334
335       if (!isValid) {
336         return handleError(request, content, Status.FORBIDDEN);
337       }
338
339       SearchOperationResult result = documentStore.search(index, queryText);
340       String output = null;
341       if (result.getResultCode() >= 200 && result.getResultCode() <= 299) {
342         output = mapper.writerWithDefaultPrettyPrinter()
343             .writeValueAsString(result.getSearchResult());
344       } else {
345         output = result.getError() != null
346             ? mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result.getError())
347             : result.getFailureCause();
348       }
349       Response response = Response.status(result.getResultCode()).entity(output).build();
350
351       // Clear the MDC context so that no other transaction inadvertently
352       // uses our transaction id.
353       ApiUtils.clearMdcContext();
354
355       return response;
356     } catch (Exception e) {
357       return handleError(request, e.getMessage(), Status.INTERNAL_SERVER_ERROR);
358     }
359   }
360
361   public Response queryWithGetWithPayload(String content, HttpServletRequest request,
362                                           HttpHeaders headers, String index,
363                                           DocumentStoreInterface documentStore) {
364
365     // Initialize the MDC Context for logging purposes.
366     ApiUtils.initMdcContext(request, headers);
367
368     logger.info(SearchDbMsgs.PROCESS_PAYLOAD_QUERY, "GET", (request != null)
369         ? request.getRequestURL().toString() : "");
370     if (logger.isDebugEnabled()) {
371       logger.debug("Request Body: " + content);
372     }
373     return processQuery(index, content, request, headers, documentStore);
374   }
375
376   public Response processSearchWithPost(String content, HttpServletRequest request,
377                                         HttpHeaders headers, String index,
378                                         DocumentStoreInterface documentStore) {
379
380     // Initialize the MDC Context for logging purposes.
381     ApiUtils.initMdcContext(request, headers);
382
383     logger.info(SearchDbMsgs.PROCESS_PAYLOAD_QUERY, "POST", (request != null)
384         ? request.getRequestURL().toString() : "");
385     if (logger.isDebugEnabled()) {
386       logger.debug("Request Body: " + content);
387     }
388
389     return processQuery(index, content, request, headers, documentStore);
390   }
391
392   /**
393    * Common handler for query requests. This is called by both the GET with
394    * payload and POST with payload variants of the query endpoint.
395    *
396    * @param index   - The index to be queried against.
397    * @param content - The payload containing the query structure.
398    * @param request - The HTTP request.
399    * @param headers - The HTTP headers.
400    * @return - A standard HTTP response.
401    */
402   private Response processQuery(String index, String content, HttpServletRequest request,
403                                 HttpHeaders headers, DocumentStoreInterface documentStore) {
404
405     try {
406       ObjectMapper mapper = new ObjectMapper();
407       mapper.setSerializationInclusion(Include.NON_EMPTY);
408
409       // Make sure that we were supplied a payload before proceeding.
410       if (content == null) {
411         return handleError(request, content, Status.BAD_REQUEST);
412       }
413
414       // Validate that the request has the appropriate authorization.
415       boolean isValid;
416       try {
417         isValid = searchService.validateRequest(headers, request, ApiUtils.Action.POST,
418             ApiUtils.SEARCH_AUTH_POLICY_NAME);
419
420       } catch (Exception e) {
421         logger.info(SearchDbMsgs.EXCEPTION_DURING_METHOD_CALL,
422             "processQuery",
423             e.getMessage());
424         return handleError(request, content, Status.FORBIDDEN);
425       }
426
427       if (!isValid) {
428         return handleError(request, content, Status.FORBIDDEN);
429       }
430
431       SearchStatement searchStatement;
432
433       try {
434         // Marshall the supplied request payload into a search statement
435         // object.
436         searchStatement = mapper.readValue(content, SearchStatement.class);
437
438       } catch (Exception e) {
439         return handleError(request, e.getMessage(), Status.BAD_REQUEST);
440       }
441
442       // Now, submit the search statement, translated into
443       // ElasticSearch syntax, to the document store DAO.
444       SearchOperationResult result = documentStore.searchWithPayload(index,
445           searchStatement.toElasticSearch());
446       String output = null;
447       if (result.getResultCode() >= 200 && result.getResultCode() <= 299) {
448         output = prepareOutput(mapper, result);
449       } else {
450         output = result.getError() != null
451             ? mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result.getError())
452             : result.getFailureCause();
453       }
454       Response response = Response.status(result.getResultCode()).entity(output).build();
455
456       // Clear the MDC context so that no other transaction inadvertently
457       // uses our transaction id.
458       ApiUtils.clearMdcContext();
459
460       return response;
461
462     } catch (Exception e) {
463       return handleError(request, e.getMessage(), Status.INTERNAL_SERVER_ERROR);
464     }
465   }
466
467   
468   /**
469    * Checks the supplied HTTP headers to see if we should allow the underlying document 
470    * store to implicitly create the index referenced in a document PUT or POST if it
471    * does not already exist in the data store.
472    * 
473    * @param headers - The HTTP headers to examine.
474    * 
475    * @return - true if the headers indicate that missing indices should be implicitly created,
476    *           false otherwise.
477    */
478   private boolean implicitlyCreateIndex(HttpHeaders headers) {
479     
480     boolean createIndexIfNotPresent = false;
481     String implicitIndexCreationHeader = 
482         headers.getRequestHeaders().getFirst(REQUEST_HEADER_ALLOW_IMPLICIT_INDEX_CREATION);
483     
484     if( (implicitIndexCreationHeader != null) && (implicitIndexCreationHeader.equals("true")) ) {
485       createIndexIfNotPresent = true;
486     }
487     
488     return createIndexIfNotPresent;
489   }
490   
491   
492   private String prepareOutput(ObjectMapper mapper, SearchOperationResult result)
493       throws JsonProcessingException {
494     StringBuffer output = new StringBuffer();
495     output.append("{\r\n\"searchResult\":");
496     output.append(mapper.writerWithDefaultPrettyPrinter()
497         .writeValueAsString(result.getSearchResult()));
498     AggregationResults aggs = result.getAggregationResult();
499     if (aggs != null) {
500       output.append(",\r\n\"aggregationResult\":");
501       output.append(mapper.setSerializationInclusion(Include.NON_NULL)
502           .writerWithDefaultPrettyPrinter().writeValueAsString(aggs));
503     }
504     output.append("\r\n}");
505     return output.toString();
506   }
507
508   private Response handleError(HttpServletRequest request, String message, Status status) {
509     logResult(request, status);
510     return Response.status(status).entity(message).type(MediaType.APPLICATION_JSON).build();
511   }
512
513   void logResult(HttpServletRequest request, Response.Status status) {
514
515     logger.info(SearchDbMsgs.PROCESS_REST_REQUEST, (request != null) ? request.getMethod() : "",
516         (request != null) ? request.getRequestURL().toString() : "",
517         (request != null) ? request.getRemoteHost() : "", Integer.toString(status.getStatusCode()));
518
519     auditLogger.info(SearchDbMsgs.PROCESS_REST_REQUEST,
520         new LogFields()
521             .setField(LogLine.DefinedFields.RESPONSE_CODE, status.getStatusCode())
522             .setField(LogLine.DefinedFields.RESPONSE_DESCRIPTION, status.getReasonPhrase()),
523         (request != null) ? request.getMethod() : "",
524         (request != null) ? request.getRequestURL().toString() : "",
525         (request != null) ? request.getRemoteHost() : "", Integer.toString(status.getStatusCode()));
526
527     // Clear the MDC context so that no other transaction inadvertently
528     // uses our transaction id.
529     ApiUtils.clearMdcContext();
530   }
531 }