Fix potential null pointer places
[aai/sparky-be.git] / sparkybe-onap-service / src / main / java / org / onap / aai / sparky / viewandinspect / search / ViewInspectSearchProvider.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.sparky.viewandinspect.search;
22
23 import java.io.IOException;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.Map;
30
31 import org.onap.aai.cl.api.Logger;
32 import org.onap.aai.cl.eelf.LoggerFactory;
33 import org.onap.aai.restclient.client.OperationResult;
34 import org.onap.aai.sparky.common.search.CommonSearchSuggestion;
35 import org.onap.aai.sparky.config.oxm.OxmEntityDescriptor;
36 import org.onap.aai.sparky.config.oxm.OxmEntityLookup;
37 import org.onap.aai.sparky.logging.AaiUiMsgs;
38 import org.onap.aai.sparky.search.SearchServiceAdapter;
39 import org.onap.aai.sparky.search.api.SearchProvider;
40 import org.onap.aai.sparky.search.config.SuggestionConfig;
41 import org.onap.aai.sparky.search.entity.QuerySearchEntity;
42 import org.onap.aai.sparky.search.entity.SearchSuggestion;
43 import org.onap.aai.sparky.util.NodeUtils;
44 import org.onap.aai.sparky.viewandinspect.config.SparkyConstants;
45
46 import com.fasterxml.jackson.databind.JsonNode;
47 import com.fasterxml.jackson.databind.ObjectMapper;
48 import com.fasterxml.jackson.databind.node.ArrayNode;
49
50 public class ViewInspectSearchProvider implements SearchProvider {
51
52   private static final Logger LOG =
53       LoggerFactory.getInstance().getLogger(ViewInspectSearchProvider.class);
54
55   private SearchServiceAdapter searchServiceAdapter = null;
56   private SuggestionConfig suggestionConfig; 
57   private String additionalSearchSuggestionText;
58   
59   private static final String KEY_SEARCH_RESULT = "searchResult";
60   private static final String KEY_HITS = "hits";
61   private static final String KEY_DOCUMENT = "document";
62   private static final String KEY_CONTENT = "content";
63
64   private static final String KEY_SEARCH_TAG_IDS = "searchTagIDs";
65   private static final String KEY_SEARCH_TAGS = "searchTags";
66   private static final String KEY_LINK = "link";
67   private static final String KEY_ENTITY_TYPE = "entityType";
68
69   private final String viewInspectIndexName;
70   private final String viewInspectSuggestionRoute;
71   private OxmEntityLookup oxmEntityLookup;
72   
73   public ViewInspectSearchProvider(SearchServiceAdapter searchServiceAdapter,
74       SuggestionConfig suggestionConfig, String viewInspectIndexName,
75       String viewInspectSuggestionRoute, OxmEntityLookup oxmEntityLookup) throws Exception {
76
77     this.searchServiceAdapter = searchServiceAdapter;
78     this.oxmEntityLookup = oxmEntityLookup;
79     this.suggestionConfig = suggestionConfig;
80     additionalSearchSuggestionText = null;
81     this.viewInspectIndexName = viewInspectIndexName;
82     this.viewInspectSuggestionRoute = viewInspectSuggestionRoute;
83
84   }
85   
86   @Override
87   public List<SearchSuggestion> search(QuerySearchEntity queryRequest) {
88
89     List<SearchSuggestion> suggestionEntityList = new ArrayList<SearchSuggestion>();
90     
91     /*
92      * Based on the configured stop words, we need to strip any matched stop-words ( case
93      * insensitively ) from the query string, before hitting elastic to prevent the words from being
94      * used against the elastic view-and-inspect index. Another alternative to this approach would
95      * be to define stop words on the elastic search index configuration for the
96      * entity-search-index, but but that may be more complicated / more risky than just a simple bug
97      * fix, but it's something we should think about for the future.
98      */
99
100     try {
101       final String queryStringWithoutStopWords =
102           stripStopWordsFromQuery(queryRequest.getQueryStr());
103
104       final String fullUrlStr = searchServiceAdapter.buildSearchServiceQueryUrl(viewInspectIndexName); 
105
106       String postBody = String.format(VIUI_SEARCH_TEMPLATE, Integer.parseInt(queryRequest.getMaxResults()),
107           queryStringWithoutStopWords);
108
109       OperationResult opResult = searchServiceAdapter.doPost(fullUrlStr, postBody, "application/json");
110       if (opResult.getResultCode() == 200) {
111         suggestionEntityList =
112             generateSuggestionsForSearchResponse(opResult.getResult(), queryRequest.getQueryStr());
113       }
114     } catch (Exception exc) {
115       LOG.error(AaiUiMsgs.SEARCH_SERVLET_ERROR,
116           "View and inspect query failed with error = " + exc.getMessage());
117     }
118     return suggestionEntityList;
119
120
121   }
122   
123   public String getAdditionalSearchSuggestionText() {
124     return additionalSearchSuggestionText;
125   }
126
127   public void setAdditionalSearchSuggestionText(String additionalSearchSuggestionText) {
128     this.additionalSearchSuggestionText = additionalSearchSuggestionText;
129   }
130
131   
132
133   
134   /**
135    * Builds the search response.
136    *
137    * @param operationResult The Elasticsearch query result
138    * @param queryStr The string the user typed into the search bar
139    * @return A list of search suggestions and corresponding UI filter values
140    */
141   private List<SearchSuggestion> generateSuggestionsForSearchResponse(String operationResult,
142       String queryStr) {
143
144
145     if (operationResult == null || operationResult.length() == 0) {
146       return null;
147     }
148
149     ObjectMapper mapper = new ObjectMapper();
150     JsonNode rootNode = null;
151     List<SearchSuggestion> suggestionEntityList = new ArrayList<SearchSuggestion>();
152     try {
153       rootNode = mapper.readTree(operationResult);
154
155       JsonNode hitsNode = rootNode.get(KEY_SEARCH_RESULT);
156       
157
158
159       // Check if there are hits that are coming back
160       if (hitsNode.has(KEY_HITS)) {
161         ArrayNode hitsArray = (ArrayNode) hitsNode.get(KEY_HITS);
162
163         /*
164          * next we iterate over the values in the hit array elements
165          */
166
167         Iterator<JsonNode> nodeIterator = hitsArray.elements();
168         JsonNode entityNode = null;
169         CommonSearchSuggestion suggestionEntity = null;
170         JsonNode sourceNode = null;
171         while (nodeIterator.hasNext()) {
172           entityNode = nodeIterator.next();
173           sourceNode = entityNode.get(KEY_DOCUMENT).get(KEY_CONTENT);
174
175           // do the point transformation as we build the response?
176           suggestionEntity = new CommonSearchSuggestion();
177           suggestionEntity.setRoute(viewInspectSuggestionRoute);
178
179           /*
180            * This is where we probably want to annotate the search tags because we also have access
181            * to the seachTagIds
182            */
183
184           String searchTagIds = getValueFromNode(sourceNode, KEY_SEARCH_TAG_IDS);
185           String searchTags = getValueFromNode(sourceNode, KEY_SEARCH_TAGS);
186           String entityType = getValueFromNode(sourceNode, KEY_ENTITY_TYPE);
187           String link = getValueFromNode(sourceNode, KEY_LINK);
188
189           if (link != null) {
190             suggestionEntity.setHashId(NodeUtils.generateUniqueShaDigest(link));
191           }
192
193           try {
194             suggestionEntity
195                 .setText(annotateSearchTags(searchTags, searchTagIds, entityType, queryStr));
196           } catch (Exception exc) {
197             String searchTagsAsText = searchTags != null ? searchTags.toString() : "n/a";
198             LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, searchTagsAsText,
199                 exc.getLocalizedMessage());
200             // at least send back the un-annotated search tags
201             suggestionEntity.setText(searchTags);
202           }
203           
204           if ( getAdditionalSearchSuggestionText() != null ) {
205             String suggestionText = suggestionEntity.getText() ;
206             suggestionText += SparkyConstants.SUGGESTION_TEXT_SEPARATOR
207                 + getAdditionalSearchSuggestionText();
208             suggestionEntity.setText(suggestionText);
209           }
210
211           if (searchTags != null) {
212             suggestionEntityList.add(suggestionEntity);
213           }
214
215         }
216       }
217     } catch (IOException exc) {
218       LOG.warn(AaiUiMsgs.SEARCH_RESPONSE_BUILDING_EXCEPTION, exc.getLocalizedMessage());
219     }
220     return suggestionEntityList;
221   }
222   
223   
224   
225   /**
226    * The current format of an UI-dropdown-item is like: "search-terms  entityType  att1=attr1_val".
227    * Example, for pserver: search-terms pserver hostname=djmAG-72060,
228    * pserver-name2=example-pserver-name2-val-17254, pserver-id=example-pserver-id-val-17254,
229    * ipv4-oam-address=example-ipv4-oam-address-val-17254 SearchController.js parses the above
230    * format. So if you are modifying the parsing below, please update SearchController.js as well.
231    *
232    * @param searchTags the search tags
233    * @param searchTagIds the search tag ids
234    * @param entityType the entity type
235    * @param queryStr the query str
236    * @return the string
237    */
238
239   private String annotateSearchTags(String searchTags, String searchTagIds, String entityType,
240       String queryStr) {
241
242     if (searchTags == null || searchTagIds == null) {
243       String valueOfSearchTags = String.valueOf(searchTags);
244       String valueOfSearchTagIds = String.valueOf(searchTagIds);
245
246       LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, "See error",
247           "Search tags = " + valueOfSearchTags + " and Seach tag IDs = " + valueOfSearchTagIds);
248       return searchTags;
249     }
250
251     if (entityType == null) {
252       LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, searchTags.toString(), "EntityType is null");
253       return searchTags;
254     }
255
256     if (queryStr == null) {
257       LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, searchTags.toString(),
258           "Query string is null");
259       return searchTags;
260     }
261
262     /*
263      * The ElasticSearch analyzer has already applied the lowercase filter, so we don't have to
264      * covert them again
265      */
266     String[] searchTagsArray = searchTags.split(";");
267     String[] searchTagIdsArray = searchTagIds.split(";");
268
269     // specifically apply lower case to the the query terms to make matching
270     // simpler
271     String[] queryTerms = queryStr.toLowerCase().split(" ");
272
273     OxmEntityDescriptor desc = oxmEntityLookup.getEntityDescriptors().get(entityType);
274
275     if (desc == null) {
276       LOG.error(AaiUiMsgs.ENTITY_NOT_FOUND_IN_OXM, entityType.toString());
277       return searchTags;
278     }
279
280     String primaryKeyName = NodeUtils.concatArray(desc.getPrimaryKeyAttributeNames(), "/");
281     String primaryKeyValue = null;
282
283     /*
284      * For each used attribute, get the fieldName for the attribute index and transform the search
285      * tag into t1,t2,t3 => h1=t1, h2=t2, h3=t3;
286      */
287     StringBuilder searchTagsBuilder = new StringBuilder(128);
288     searchTagsBuilder.append(entityType);
289
290     String primaryKeyConjunctionValue = null;
291     boolean queryTermsMatchedSearchTags = false;
292
293     if (searchTagsArray.length == searchTagIdsArray.length) {
294       for (int i = 0; i < searchTagsArray.length; i++) {
295         String searchTagAttributeId = searchTagIdsArray[i];
296         String searchTagAttributeValue = searchTagsArray[i];
297
298         // Find the concat conjunction
299         Map<String, String> pairConjunctionList = suggestionConfig.getPairingList();
300
301         String suggConjunction = null;
302         if (pairConjunctionList.get(searchTagAttributeId) != null) {
303           suggConjunction = pairConjunctionList.get(searchTagAttributeId);
304         } else {
305           suggConjunction = suggestionConfig.getDefaultPairingValue();
306         }
307
308         if (primaryKeyName.equals(searchTagAttributeId)) {
309           primaryKeyValue = searchTagAttributeValue;
310           primaryKeyConjunctionValue = suggConjunction;
311         }
312
313         if (queryTermsMatchSearchTag(queryTerms, searchTagAttributeValue)) {
314           searchTagsBuilder.append(" " + suggConjunction + " " + searchTagAttributeValue);
315           queryTermsMatchedSearchTags = true;
316         }
317       }
318     } else {
319       String errorMessage =
320           "Search tags length did not match search tag ID length for entity type " + entityType;
321       LOG.error(AaiUiMsgs.ENTITY_SYNC_SEARCH_TAG_ANNOTATION_FAILED, errorMessage);
322     }
323     
324     
325
326     /*
327      * if none of the user query terms matched the index entity search tags then we should still tag
328      * the matched entity with a conjunction set to at least it's entity primary key value to
329      * discriminate between the entities of the same type in the search results displayed in the UI
330      * search bar results
331      */
332
333     if (!queryTermsMatchedSearchTags) {
334
335       if (primaryKeyValue != null && primaryKeyConjunctionValue != null) {
336         searchTagsBuilder.append(" " + primaryKeyConjunctionValue + " " + primaryKeyValue);
337       } else {
338         LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, "See error",
339             "Could not annotate user query terms " + queryStr
340                 + " from available entity search tags = " + searchTags);
341         return searchTags;
342       }
343
344     }
345
346     return searchTagsBuilder.toString();
347
348   }
349   
350   /**
351    * Query terms match search tag.
352    *
353    * @param queryTerms the query terms
354    * @param searchTag the search tag
355    * @return true, if successful @return.
356    */
357   private boolean queryTermsMatchSearchTag(String[] queryTerms, String searchTag) {
358
359     if (queryTerms == null || queryTerms.length == 0 || searchTag == null) {
360       return false;
361     }
362
363     for (String queryTerm : queryTerms) {
364       if (searchTag.toLowerCase().contains(queryTerm.toLowerCase())) {
365         return true;
366       }
367     }
368
369     return false;
370
371   }
372   
373   /**
374    * Gets the value from node.
375    *
376    * @param node the node
377    * @param fieldName the field name
378    * @return the value from node
379    */
380   private String getValueFromNode(JsonNode node, String fieldName) {
381
382     if (node == null || fieldName == null) {
383       return null;
384     }
385
386     JsonNode valueNode = node.get(fieldName);
387
388     if (valueNode != null) {
389       return valueNode.asText();
390     }
391
392     return null;
393
394   }
395   
396   private static final String VIUI_SEARCH_TEMPLATE =
397       "{ " + "\"results-start\": 0," + "\"results-size\": %d," + "\"queries\": [{" + "\"must\": {"
398           + "\"match\": {" + "\"field\": \"entityType searchTags crossEntityReferenceValues\","
399           + "\"value\": \"%s\"," + "\"operator\": \"and\", "
400           + "\"analyzer\": \"whitespace_analyzer\"" + "}" + "}" + "}]" + "}";
401   
402  //private SuggestionConfig suggestionConfig = null;
403   
404   /**
405    * @param queryStr - space separate query search terms
406    * @return - query string with stop-words removed
407    */
408   private String stripStopWordsFromQuery(String queryStr) {
409
410     if (queryStr == null) {
411       return queryStr;
412     }
413
414     Collection<String> stopWords = suggestionConfig.getStopWords();
415     ArrayList<String> queryTerms =
416         new ArrayList<String>(Arrays.asList(queryStr.toLowerCase().split(" ")));
417
418     queryTerms.removeAll(stopWords);
419
420     return String.join(" ", queryTerms);
421   }
422
423 }