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);
 
 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             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);
 
 204           if ( getAdditionalSearchSuggestionText() != null ) {
 
 205             String suggestionText = suggestionEntity.getText() ;
 
 206             suggestionText += SparkyConstants.SUGGESTION_TEXT_SEPARATOR
 
 207                 + getAdditionalSearchSuggestionText();
 
 208             suggestionEntity.setText(suggestionText);
 
 211           if (searchTags != null) {
 
 212             suggestionEntityList.add(suggestionEntity);
 
 217     } catch (IOException exc) {
 
 218       LOG.warn(AaiUiMsgs.SEARCH_RESPONSE_BUILDING_EXCEPTION, exc.getLocalizedMessage());
 
 220     return suggestionEntityList;
 
 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.
 
 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
 
 239   private String annotateSearchTags(String searchTags, String searchTagIds, String entityType,
 
 242     if (searchTags == null || searchTagIds == null) {
 
 243       String valueOfSearchTags = String.valueOf(searchTags);
 
 244       String valueOfSearchTagIds = String.valueOf(searchTagIds);
 
 246       LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, "See error",
 
 247           "Search tags = " + valueOfSearchTags + " and Seach tag IDs = " + valueOfSearchTagIds);
 
 251     if (entityType == null) {
 
 252       LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, searchTags.toString(), "EntityType is null");
 
 256     if (queryStr == null) {
 
 257       LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, searchTags.toString(),
 
 258           "Query string is null");
 
 263      * The ElasticSearch analyzer has already applied the lowercase filter, so we don't have to
 
 266     String[] searchTagsArray = searchTags.split(";");
 
 267     String[] searchTagIdsArray = searchTagIds.split(";");
 
 269     // specifically apply lower case to the the query terms to make matching
 
 271     String[] queryTerms = queryStr.toLowerCase().split(" ");
 
 273     OxmEntityDescriptor desc = oxmEntityLookup.getEntityDescriptors().get(entityType);
 
 276       LOG.error(AaiUiMsgs.ENTITY_NOT_FOUND_IN_OXM, entityType.toString());
 
 280     String primaryKeyName = NodeUtils.concatArray(desc.getPrimaryKeyAttributeNames(), "/");
 
 281     String primaryKeyValue = null;
 
 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;
 
 287     StringBuilder searchTagsBuilder = new StringBuilder(128);
 
 288     searchTagsBuilder.append(entityType);
 
 290     String primaryKeyConjunctionValue = null;
 
 291     boolean queryTermsMatchedSearchTags = false;
 
 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];
 
 298         // Find the concat conjunction
 
 299         Map<String, String> pairConjunctionList = suggestionConfig.getPairingList();
 
 301         String suggConjunction = null;
 
 302         if (pairConjunctionList.get(searchTagAttributeId) != null) {
 
 303           suggConjunction = pairConjunctionList.get(searchTagAttributeId);
 
 305           suggConjunction = suggestionConfig.getDefaultPairingValue();
 
 308         if (primaryKeyName.equals(searchTagAttributeId)) {
 
 309           primaryKeyValue = searchTagAttributeValue;
 
 310           primaryKeyConjunctionValue = suggConjunction;
 
 313         if (queryTermsMatchSearchTag(queryTerms, searchTagAttributeValue)) {
 
 314           searchTagsBuilder.append(" " + suggConjunction + " " + searchTagAttributeValue);
 
 315           queryTermsMatchedSearchTags = true;
 
 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);
 
 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
 
 333     if (!queryTermsMatchedSearchTags) {
 
 335       if (primaryKeyValue != null && primaryKeyConjunctionValue != null) {
 
 336         searchTagsBuilder.append(" " + primaryKeyConjunctionValue + " " + primaryKeyValue);
 
 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);
 
 346     return searchTagsBuilder.toString();
 
 351    * Query terms match search tag.
 
 353    * @param queryTerms the query terms
 
 354    * @param searchTag the search tag
 
 355    * @return true, if successful @return.
 
 357   private boolean queryTermsMatchSearchTag(String[] queryTerms, String searchTag) {
 
 359     if (queryTerms == null || queryTerms.length == 0 || searchTag == null) {
 
 363     for (String queryTerm : queryTerms) {
 
 364       if (searchTag.toLowerCase().contains(queryTerm.toLowerCase())) {
 
 374    * Gets the value from node.
 
 376    * @param node the node
 
 377    * @param fieldName the field name
 
 378    * @return the value from node
 
 380   private String getValueFromNode(JsonNode node, String fieldName) {
 
 382     if (node == null || fieldName == null) {
 
 386     JsonNode valueNode = node.get(fieldName);
 
 388     if (valueNode != null) {
 
 389       return valueNode.asText();
 
 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\"" + "}" + "}" + "}]" + "}";
 
 402  //private SuggestionConfig suggestionConfig = null;
 
 405    * @param queryStr - space separate query search terms
 
 406    * @return - query string with stop-words removed
 
 408   private String stripStopWordsFromQuery(String queryStr) {
 
 410     if (queryStr == null) {
 
 414     Collection<String> stopWords = suggestionConfig.getStopWords();
 
 415     ArrayList<String> queryTerms =
 
 416         new ArrayList<String>(Arrays.asList(queryStr.toLowerCase().split(" ")));
 
 418     queryTerms.removeAll(stopWords);
 
 420     return String.join(" ", queryTerms);