2 * ============LICENSE_START=======================================================
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
12 * http://www.apache.org/licenses/LICENSE-2.0
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=========================================================
21 package org.onap.aai.sparky.viewandinspect.search;
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;
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;
46 import com.fasterxml.jackson.databind.JsonNode;
47 import com.fasterxml.jackson.databind.ObjectMapper;
48 import com.fasterxml.jackson.databind.node.ArrayNode;
50 public class ViewInspectSearchProvider implements SearchProvider {
52 private static final Logger LOG =
53 LoggerFactory.getInstance().getLogger(ViewInspectSearchProvider.class);
55 private SearchServiceAdapter searchServiceAdapter = null;
56 private SuggestionConfig suggestionConfig;
57 private String additionalSearchSuggestionText;
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";
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";
69 private final String viewInspectIndexName;
70 private final String viewInspectSuggestionRoute;
71 private OxmEntityLookup oxmEntityLookup;
73 public ViewInspectSearchProvider(SearchServiceAdapter searchServiceAdapter,
74 SuggestionConfig suggestionConfig, String viewInspectIndexName,
75 String viewInspectSuggestionRoute, OxmEntityLookup oxmEntityLookup) throws Exception {
77 this.searchServiceAdapter = searchServiceAdapter;
78 this.oxmEntityLookup = oxmEntityLookup;
79 this.suggestionConfig = suggestionConfig;
80 additionalSearchSuggestionText = null;
81 this.viewInspectIndexName = viewInspectIndexName;
82 this.viewInspectSuggestionRoute = viewInspectSuggestionRoute;
87 public List<SearchSuggestion> search(QuerySearchEntity queryRequest) {
89 List<SearchSuggestion> suggestionEntityList = new ArrayList<SearchSuggestion>();
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.
101 final String queryStringWithoutStopWords =
102 stripStopWordsFromQuery(queryRequest.getQueryStr());
104 final String fullUrlStr = searchServiceAdapter.buildSearchServiceQueryUrl(viewInspectIndexName);
106 String postBody = String.format(VIUI_SEARCH_TEMPLATE, Integer.parseInt(queryRequest.getMaxResults()),
107 queryStringWithoutStopWords);
109 OperationResult opResult = searchServiceAdapter.doPost(fullUrlStr, postBody, "application/json");
110 if (opResult.getResultCode() == 200) {
111 suggestionEntityList =
112 generateSuggestionsForSearchResponse(opResult.getResult(), queryRequest.getQueryStr());
114 } catch (Exception exc) {
115 LOG.error(AaiUiMsgs.SEARCH_SERVLET_ERROR,
116 "View and inspect query failed with error = " + exc.getMessage());
118 return suggestionEntityList;
123 public String getAdditionalSearchSuggestionText() {
124 return additionalSearchSuggestionText;
127 public void setAdditionalSearchSuggestionText(String additionalSearchSuggestionText) {
128 this.additionalSearchSuggestionText = additionalSearchSuggestionText;
135 * Builds the search response.
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
141 private List<SearchSuggestion> generateSuggestionsForSearchResponse(String operationResult,
145 if (operationResult == null || operationResult.length() == 0) {
149 ObjectMapper mapper = new ObjectMapper();
150 JsonNode rootNode = null;
151 List<SearchSuggestion> suggestionEntityList = new ArrayList<SearchSuggestion>();
153 rootNode = mapper.readTree(operationResult);
155 JsonNode hitsNode = rootNode.get(KEY_SEARCH_RESULT);
159 // Check if there are hits that are coming back
160 if (hitsNode.has(KEY_HITS)) {
161 ArrayNode hitsArray = (ArrayNode) hitsNode.get(KEY_HITS);
164 * next we iterate over the values in the hit array elements
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);
175 // do the point transformation as we build the response?
176 suggestionEntity = new CommonSearchSuggestion();
177 suggestionEntity.setRoute(viewInspectSuggestionRoute);
180 * This is where we probably want to annotate the search tags because we also have access
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);
190 suggestionEntity.setHashId(NodeUtils.generateUniqueShaDigest(link));
195 .setText(annotateSearchTags(searchTags, searchTagIds, entityType, queryStr));
196 } catch (Exception exc) {
197 LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, searchTags.toString(),
198 exc.getLocalizedMessage());
199 // at least send back the un-annotated search tags
200 suggestionEntity.setText(searchTags);
203 if ( getAdditionalSearchSuggestionText() != null ) {
204 String suggestionText = suggestionEntity.getText() ;
205 suggestionText += SparkyConstants.SUGGESTION_TEXT_SEPARATOR
206 + getAdditionalSearchSuggestionText();
207 suggestionEntity.setText(suggestionText);
210 if (searchTags != null) {
211 suggestionEntityList.add(suggestionEntity);
216 } catch (IOException exc) {
217 LOG.warn(AaiUiMsgs.SEARCH_RESPONSE_BUILDING_EXCEPTION, exc.getLocalizedMessage());
219 return suggestionEntityList;
225 * The current format of an UI-dropdown-item is like: "search-terms entityType att1=attr1_val".
226 * Example, for pserver: search-terms pserver hostname=djmAG-72060,
227 * pserver-name2=example-pserver-name2-val-17254, pserver-id=example-pserver-id-val-17254,
228 * ipv4-oam-address=example-ipv4-oam-address-val-17254 SearchController.js parses the above
229 * format. So if you are modifying the parsing below, please update SearchController.js as well.
231 * @param searchTags the search tags
232 * @param searchTagIds the search tag ids
233 * @param entityType the entity type
234 * @param queryStr the query str
238 private String annotateSearchTags(String searchTags, String searchTagIds, String entityType,
241 if (searchTags == null || searchTagIds == null) {
242 String valueOfSearchTags = String.valueOf(searchTags);
243 String valueOfSearchTagIds = String.valueOf(searchTagIds);
245 LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, "See error",
246 "Search tags = " + valueOfSearchTags + " and Seach tag IDs = " + valueOfSearchTagIds);
250 if (entityType == null) {
251 LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, searchTags.toString(), "EntityType is null");
255 if (queryStr == null) {
256 LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, searchTags.toString(),
257 "Query string is null");
262 * The ElasticSearch analyzer has already applied the lowercase filter, so we don't have to
265 String[] searchTagsArray = searchTags.split(";");
266 String[] searchTagIdsArray = searchTagIds.split(";");
268 // specifically apply lower case to the the query terms to make matching
270 String[] queryTerms = queryStr.toLowerCase().split(" ");
272 OxmEntityDescriptor desc = oxmEntityLookup.getEntityDescriptors().get(entityType);
275 LOG.error(AaiUiMsgs.ENTITY_NOT_FOUND_IN_OXM, entityType.toString());
279 String primaryKeyName = NodeUtils.concatArray(desc.getPrimaryKeyAttributeNames(), "/");
280 String primaryKeyValue = null;
283 * For each used attribute, get the fieldName for the attribute index and transform the search
284 * tag into t1,t2,t3 => h1=t1, h2=t2, h3=t3;
286 StringBuilder searchTagsBuilder = new StringBuilder(128);
287 searchTagsBuilder.append(entityType);
289 String primaryKeyConjunctionValue = null;
290 boolean queryTermsMatchedSearchTags = false;
292 if (searchTagsArray.length == searchTagIdsArray.length) {
293 for (int i = 0; i < searchTagsArray.length; i++) {
294 String searchTagAttributeId = searchTagIdsArray[i];
295 String searchTagAttributeValue = searchTagsArray[i];
297 // Find the concat conjunction
298 Map<String, String> pairConjunctionList = suggestionConfig.getPairingList();
300 String suggConjunction = null;
301 if (pairConjunctionList.get(searchTagAttributeId) != null) {
302 suggConjunction = pairConjunctionList.get(searchTagAttributeId);
304 suggConjunction = suggestionConfig.getDefaultPairingValue();
307 if (primaryKeyName.equals(searchTagAttributeId)) {
308 primaryKeyValue = searchTagAttributeValue;
309 primaryKeyConjunctionValue = suggConjunction;
312 if (queryTermsMatchSearchTag(queryTerms, searchTagAttributeValue)) {
313 searchTagsBuilder.append(" " + suggConjunction + " " + searchTagAttributeValue);
314 queryTermsMatchedSearchTags = true;
318 String errorMessage =
319 "Search tags length did not match search tag ID length for entity type " + entityType;
320 LOG.error(AaiUiMsgs.ENTITY_SYNC_SEARCH_TAG_ANNOTATION_FAILED, errorMessage);
326 * if none of the user query terms matched the index entity search tags then we should still tag
327 * the matched entity with a conjunction set to at least it's entity primary key value to
328 * discriminate between the entities of the same type in the search results displayed in the UI
332 if (!queryTermsMatchedSearchTags) {
334 if (primaryKeyValue != null && primaryKeyConjunctionValue != null) {
335 searchTagsBuilder.append(" " + primaryKeyConjunctionValue + " " + primaryKeyValue);
337 LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, "See error",
338 "Could not annotate user query terms " + queryStr
339 + " from available entity search tags = " + searchTags);
345 return searchTagsBuilder.toString();
350 * Query terms match search tag.
352 * @param queryTerms the query terms
353 * @param searchTag the search tag
354 * @return true, if successful @return.
356 private boolean queryTermsMatchSearchTag(String[] queryTerms, String searchTag) {
358 if (queryTerms == null || queryTerms.length == 0 || searchTag == null) {
362 for (String queryTerm : queryTerms) {
363 if (searchTag.toLowerCase().contains(queryTerm.toLowerCase())) {
373 * Gets the value from node.
375 * @param node the node
376 * @param fieldName the field name
377 * @return the value from node
379 private String getValueFromNode(JsonNode node, String fieldName) {
381 if (node == null || fieldName == null) {
385 JsonNode valueNode = node.get(fieldName);
387 if (valueNode != null) {
388 return valueNode.asText();
395 private static final String VIUI_SEARCH_TEMPLATE =
396 "{ " + "\"results-start\": 0," + "\"results-size\": %d," + "\"queries\": [{" + "\"must\": {"
397 + "\"match\": {" + "\"field\": \"entityType searchTags crossEntityReferenceValues\","
398 + "\"value\": \"%s\"," + "\"operator\": \"and\", "
399 + "\"analyzer\": \"whitespace_analyzer\"" + "}" + "}" + "}]" + "}";
401 //private SuggestionConfig suggestionConfig = null;
404 * @param queryStr - space separate query search terms
405 * @return - query string with stop-words removed
407 private String stripStopWordsFromQuery(String queryStr) {
409 if (queryStr == null) {
413 Collection<String> stopWords = suggestionConfig.getStopWords();
414 ArrayList<String> queryTerms =
415 new ArrayList<String>(Arrays.asList(queryStr.toLowerCase().split(" ")));
417 queryTerms.removeAll(stopWords);
419 return String.join(" ", queryTerms);