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