Sonar Bug Fix- for EntityEventPolicy.java
[aai/data-router.git] / src / main / java / org / onap / aai / datarouter / policy / EntityEventPolicy.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  * Modifications Copyright (C) 2019 IBM
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 package org.onap.aai.datarouter.policy;
23
24 import java.io.FileNotFoundException;
25 import java.io.IOException;
26 import java.security.NoSuchAlgorithmException;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.Iterator;
33 import java.util.List;
34 import java.util.Map;
35
36 import org.apache.camel.Exchange;
37 import org.apache.camel.Processor;
38 import org.eclipse.persistence.dynamic.DynamicType;
39 import org.eclipse.persistence.internal.helper.DatabaseField;
40 import org.eclipse.persistence.jaxb.dynamic.DynamicJAXBContext;
41 import org.eclipse.persistence.oxm.MediaType;
42 import org.json.JSONException;
43 import org.json.JSONObject;
44 import org.onap.aai.datarouter.entity.AaiEventEntity;
45 import org.onap.aai.datarouter.entity.AggregationEntity;
46 import org.onap.aai.datarouter.entity.DocumentStoreDataEntity;
47 import org.onap.aai.datarouter.entity.SuggestionSearchEntity;
48 import org.onap.aai.datarouter.entity.TopographicalEntity;
49 import org.onap.aai.datarouter.entity.UebEventHeader;
50 import org.onap.aai.datarouter.logging.EntityEventPolicyMsgs;
51 import org.onap.aai.datarouter.util.NodeUtils;
52 import org.onap.aai.datarouter.util.RouterServiceUtil;
53 import org.onap.aai.datarouter.util.SearchServiceAgent;
54 import org.onap.aai.datarouter.util.SearchSuggestionPermutation;
55 import org.onap.aai.entity.OxmEntityDescriptor;
56 import org.onap.aai.util.CrossEntityReference;
57 import org.onap.aai.util.EntityOxmReferenceHelper;
58 import org.onap.aai.util.ExternalOxmModelProcessor;
59 import org.onap.aai.schema.OxmModelLoader;
60 import org.onap.aai.setup.SchemaVersions;
61 import org.onap.aai.util.Version;
62 import org.onap.aai.util.VersionedOxmEntities;
63 import org.onap.aai.cl.api.Logger;
64 import org.onap.aai.cl.eelf.LoggerFactory;
65 import org.onap.aai.cl.mdc.MdcContext;
66 import org.onap.aai.restclient.client.Headers;
67 import org.onap.aai.restclient.client.OperationResult;
68 import org.onap.aai.restclient.rest.HttpUtil;
69 import org.slf4j.MDC;
70
71 import com.fasterxml.jackson.core.JsonProcessingException;
72 import com.fasterxml.jackson.databind.JsonNode;
73 import com.fasterxml.jackson.databind.ObjectMapper;
74 import com.fasterxml.jackson.databind.ObjectWriter;
75 import com.fasterxml.jackson.databind.node.ObjectNode;
76
77 public class EntityEventPolicy implements Processor {
78
79   public static final String additionalInfo = "Response of AAIEntityEventPolicy";
80   private static final String ENTITY_SEARCH_SCHEMA = "entitysearch_schema.json";
81   private static final String TOPOGRAPHICAL_SEARCH_SCHEMA = "topographysearch_schema.json";
82   private Collection<ExternalOxmModelProcessor> externalOxmModelProcessors;
83
84   private static final String EVENT_HEADER = "event-header";
85   private static final String ENTITY_HEADER = "entity";
86   private static final String ACTION_CREATE = "create";
87   private static final String ACTION_DELETE = "delete";
88   private static final String ACTION_UPDATE = "update";
89   private static final String PROCESS_AAI_EVENT = "Process AAI Event";
90   private static final String TOPO_LAT = "latitude";
91   private static final String TOPO_LONG = "longitude";
92
93   private static final List<String> SUPPORTED_ACTIONS =
94       Arrays.asList(ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE);
95
96   Map<String, DynamicJAXBContext> oxmVersionContextMap = new HashMap<>();
97   private String oxmVersion = null;
98
99   /** Agent for communicating with the Search Service. */
100   private SearchServiceAgent searchAgent = null;
101   
102   /** Search index name for storing AAI event entities. */
103   private String entitySearchIndex;
104
105   /** Search index name for storing topographical search data. */
106   private String topographicalSearchIndex;
107   
108   /** Search index name for suggestive search data. */
109   private String aggregateGenericVnfIndex;
110   
111   private String autosuggestIndex;
112
113   private String srcDomain;
114
115   private Logger logger;
116   private Logger metricsLogger;
117
118   public enum ResponseType {
119     SUCCESS, PARTIAL_SUCCESS, FAILURE;
120   };
121
122   public EntityEventPolicy(EntityEventPolicyConfig config) throws FileNotFoundException {
123     LoggerFactory loggerFactoryInstance = LoggerFactory.getInstance();
124     logger = loggerFactoryInstance.getLogger(EntityEventPolicy.class.getName());
125     metricsLogger = loggerFactoryInstance.getMetricsLogger(EntityEventPolicy.class.getName());
126
127
128     srcDomain = config.getSourceDomain();
129
130     // Populate the index names.
131     entitySearchIndex        = config.getSearchEntitySearchIndex();
132     topographicalSearchIndex = config.getSearchTopographySearchIndex();
133     aggregateGenericVnfIndex = config.getSearchAggregationVnfIndex();
134     autosuggestIndex             = config.getSearchEntityAutoSuggestIndex();
135        
136     // Instantiate the agent that we will use for interacting with the Search Service.
137     searchAgent = new SearchServiceAgent(config.getSearchCertName(),
138                                          config.getSearchKeystore(),
139                                          config.getSearchKeystorePwd(),
140                                          EntityEventPolicy.concatSubUri(config.getSearchBaseUrl(),
141                                                                         config.getSearchEndpoint()),
142                                          config.getSearchEndpointDocuments(),
143                                          logger);
144
145     this.externalOxmModelProcessors = new ArrayList<>();
146     this.externalOxmModelProcessors.add(EntityOxmReferenceHelper.getInstance());
147     OxmModelLoader.registerExternalOxmModelProcessors(externalOxmModelProcessors);
148     OxmModelLoader.loadModels(config.getSchemaVersions(), config.getSchemaLocationsBean());
149     oxmVersionContextMap = OxmModelLoader.getVersionContextMap();
150     parseLatestOxmVersion();
151   }
152
153   private void parseLatestOxmVersion() {
154     int latestVersion = -1;
155     if (oxmVersionContextMap != null) {
156       Iterator it = oxmVersionContextMap.entrySet().iterator();
157       while (it.hasNext()) {
158         Map.Entry pair = (Map.Entry) it.next();
159
160         String version = pair.getKey().toString();
161         int versionNum = Integer.parseInt(version.substring(1, version.length()));
162
163         if (versionNum > latestVersion) {
164           latestVersion = versionNum;
165           oxmVersion = pair.getKey().toString();
166         }
167
168         logger.info(EntityEventPolicyMsgs.PROCESS_OXM_MODEL_FOUND, pair.getKey().toString());
169       }
170     } else {
171       logger.error(EntityEventPolicyMsgs.PROCESS_OXM_MODEL_MISSING, "");
172     }
173   }
174
175   public void startup() {
176     
177     // Create the indexes in the search service if they do not already exist.
178     searchAgent.createSearchIndex(entitySearchIndex, ENTITY_SEARCH_SCHEMA);
179     searchAgent.createSearchIndex(topographicalSearchIndex, TOPOGRAPHICAL_SEARCH_SCHEMA);
180     
181     logger.info(EntityEventPolicyMsgs.ENTITY_EVENT_POLICY_REGISTERED);
182   }
183
184
185   /**
186    * Convert object to json.
187    *
188    * @param object the object
189    * @param pretty the pretty
190    * @return the string
191    * @throws JsonProcessingException the json processing exception
192    */
193   public static String convertObjectToJson(Object object, boolean pretty)
194       throws JsonProcessingException {
195     ObjectWriter ow;
196
197     if (pretty) {
198       ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
199
200     } else {
201       ow = new ObjectMapper().writer();
202     }
203
204     return ow.writeValueAsString(object);
205   }
206
207   public void returnWithError(Exchange exchange, String payload, String errorMsg){
208     logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE, errorMsg);
209     logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE, errorMsg, payload);
210     setResponse(exchange, ResponseType.FAILURE, additionalInfo);
211   }
212   
213   @Override
214   public void process(Exchange exchange) throws Exception {
215
216     long startTime = System.currentTimeMillis();
217
218     String uebPayload = exchange.getIn().getBody().toString();
219
220     JsonNode uebAsJson = null;
221     ObjectMapper mapper = new ObjectMapper();
222     try{
223       uebAsJson = mapper.readTree(uebPayload);
224     } catch (IOException e){
225       returnWithError(exchange, uebPayload, "Invalid Payload");
226       return;
227     }
228
229     // Load the UEB payload data, any errors will result in a failure and discard
230     JSONObject uebObjHeader = getUebContentAsJson(uebPayload, EVENT_HEADER);
231     if (uebObjHeader == null) {
232       returnWithError(exchange, uebPayload, "Payload is missing " + EVENT_HEADER);
233       return;
234     }
235     
236     JSONObject uebObjEntity = getUebContentAsJson(uebPayload, ENTITY_HEADER);
237     if (uebObjEntity == null) {
238       returnWithError(exchange, uebPayload, "Payload is missing " + ENTITY_HEADER);
239       return;
240     }
241
242     UebEventHeader eventHeader;
243     eventHeader = initializeUebEventHeader(uebObjHeader.toString());
244
245     // Get src domain from header; discard event if not originated from same domain
246     String payloadSrcDomain = eventHeader.getDomain();
247     if (payloadSrcDomain == null || !payloadSrcDomain.equalsIgnoreCase(this.srcDomain)) {
248       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
249           "Unrecognized source domain '" + payloadSrcDomain + "'", uebPayload);
250       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
251           "Unrecognized source domain '" + payloadSrcDomain + "'");
252
253       setResponse(exchange, ResponseType.SUCCESS, additionalInfo);
254       return;
255     }
256
257     DynamicJAXBContext oxmJaxbContext = loadOxmContext(oxmVersion);
258     if (oxmJaxbContext == null) {
259       logger.error(EntityEventPolicyMsgs.OXM_VERSION_NOT_SUPPORTED, oxmVersion);
260       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE, "OXM version mismatch",
261           uebPayload);
262
263       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
264       return;
265     }
266
267     String action = eventHeader.getAction();
268     if (action == null || !SUPPORTED_ACTIONS.contains(action.toLowerCase())) {
269       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
270           "Unrecognized action '" + action + "'", uebPayload);
271       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
272           "Unrecognized action '" + action + "'");
273
274       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
275       return;
276     }
277
278     String entityType = eventHeader.getEntityType();
279     if (entityType == null) {
280       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
281           "Payload header missing entity type", uebPayload);
282       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
283           "Payload header missing entity type");
284
285       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
286       return;
287     }
288
289     String topEntityType = eventHeader.getTopEntityType();
290     if (topEntityType == null) {
291       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
292           "Payload header missing top entity type", uebPayload);
293       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
294           "Payload header top missing entity type");
295
296       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
297       return;
298     }
299
300     String entityLink = eventHeader.getEntityLink();
301     if (entityLink == null) {
302       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
303           "Payload header missing entity link", uebPayload);
304       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
305           "Payload header missing entity link");
306
307       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
308       return;
309     }
310
311     // log the fact that all data are in good shape
312     logger.info(EntityEventPolicyMsgs.PROCESS_ENTITY_EVENT_POLICY_NONVERBOSE, action,
313         entityType);
314     logger.debug(EntityEventPolicyMsgs.PROCESS_ENTITY_EVENT_POLICY_VERBOSE, action, entityType,
315         uebPayload);
316
317
318     // Process for building AaiEventEntity object
319     String oxmEntityType = new OxmEntityTypeConverter().convert(entityType);
320
321     List<String> searchableAttr =
322         getOxmAttributes(uebPayload, oxmJaxbContext, oxmEntityType, entityType, "searchable");
323     if (searchableAttr == null) {
324       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
325           "Searchable attribute not found for payload entity type '" + entityType + "'");
326       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
327           "Searchable attribute not found for payload entity type '" + entityType + "'",
328           uebPayload);
329
330       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
331       return;
332     }
333
334     String entityPrimaryKeyFieldName =
335         getEntityPrimaryKeyFieldName(oxmJaxbContext, uebPayload, oxmEntityType, entityType);
336     String entityPrimaryKeyFieldValue = lookupValueUsingKey(uebPayload, entityPrimaryKeyFieldName);
337     if (entityPrimaryKeyFieldValue == null || entityPrimaryKeyFieldValue.isEmpty()) {
338       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
339           "Payload missing primary key attribute");
340       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
341           "Payload missing primary key attribute", uebPayload);
342
343       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
344       return;
345     }
346
347     AaiEventEntity aaiEventEntity = new AaiEventEntity();
348
349     /*
350      * Use the OXM Model to determine the primary key field name based on the entity-type
351      */
352
353     aaiEventEntity.setEntityPrimaryKeyName(entityPrimaryKeyFieldName);
354     aaiEventEntity.setEntityPrimaryKeyValue(entityPrimaryKeyFieldValue);
355     aaiEventEntity.setEntityType(entityType);
356     aaiEventEntity.setLink(entityLink);
357
358     if (!getSearchTags(aaiEventEntity, searchableAttr, uebPayload, action)) {
359       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
360           "Payload missing searchable attribute for entity type '" + entityType + "'");
361       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
362           "Payload missing searchable attribute for entity type '" + entityType + "'", uebPayload);
363
364       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
365       return;
366
367     }
368
369     try {
370       aaiEventEntity.deriveFields();
371
372     } catch (NoSuchAlgorithmException e) {
373       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
374           "Cannot create unique SHA digest");
375       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
376           "Cannot create unique SHA digest", uebPayload);
377
378       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
379       return;
380     }
381
382     handleSearchServiceOperation(aaiEventEntity, action, entitySearchIndex);
383
384     handleTopographicalData(uebPayload, action, entityType, oxmEntityType, oxmJaxbContext,
385         entityPrimaryKeyFieldName, entityPrimaryKeyFieldValue);
386
387     /*
388      * Use the versioned OXM Entity class to get access to cross-entity reference helper collections
389      */
390     VersionedOxmEntities oxmEntities =
391         EntityOxmReferenceHelper.getInstance().getVersionedOxmEntities(Version.valueOf(oxmVersion.toLowerCase()));
392
393     /**
394      * NOTES:
395      * 1. If the entity type is "customer", the below check will return true if any nested entityType
396      * in that model could contain a CER based on the OXM model version that has been loaded.
397      * 2. For a DELETE operation on outer/parent entity (handled by the regular flow: 
398      * handleSearchServiceOperation()), ignore processing for cross-entity-reference under the
399      * assumption that AAI will push down all required cascade-deletes for nested entities as well
400      * 3. Handling the case where UEB events arrive out of order: CREATE customer is received before
401      *  CREATE service-instance.
402      */
403
404     if (!action.equalsIgnoreCase(ACTION_DELETE) && oxmEntities != null 
405         && oxmEntities.entityModelContainsCrossEntityReference(topEntityType)) {
406
407       // We know the model "can" contain a CER reference definition, let's process a bit more
408
409       HashMap<String, CrossEntityReference> crossEntityRefMap =
410           oxmEntities.getCrossEntityReferences();
411
412       JSONObject entityJsonObject = getUebEntity(uebPayload);
413
414       JsonNode entityJsonNode = 
415                   convertToJsonNode(entityJsonObject !=null ? entityJsonObject.toString() : "");
416       
417       String parentEntityType = entityType;
418       
419       String targetEntityUrl = entityLink;
420
421       for (Map.Entry<String, CrossEntityReference> entry : crossEntityRefMap.entrySet()) {
422
423         /*
424          * if we know service-subscription is in the tree, then we can pull our all instances and
425          * process from there.
426          */
427
428         String key = entry.getKey();
429         CrossEntityReference cerDescriptor = entry.getValue();
430
431         ArrayList<JsonNode> foundNodes = new ArrayList<>();
432
433         RouterServiceUtil.extractObjectsByKey(entityJsonNode, key, foundNodes);
434
435         if (!foundNodes.isEmpty()) {
436
437           for (JsonNode n : foundNodes) {
438             if ("customer".equalsIgnoreCase(parentEntityType)){  
439               /*
440                * NOTES:
441                * 1. prepare to hand-create url for service-instance
442                * 2. this will break if the URL structure for service-instance changes
443                */
444               if (n.has("service-type")){
445                 targetEntityUrl += "/service-subscriptions/service-subscription/" 
446                     + RouterServiceUtil.getNodeFieldAsText(n, "service-type") 
447                     + "/service-instances/service-instance/";
448               }
449               
450             }
451
452             List<String> extractedParentEntityAttributeValues = new ArrayList<>();
453
454             RouterServiceUtil.extractFieldValuesFromObject(n, cerDescriptor.getAttributeNames(),
455                 extractedParentEntityAttributeValues);
456
457             List<JsonNode> nestedTargetEntityInstances = new ArrayList<>();
458             RouterServiceUtil.extractObjectsByKey(n, cerDescriptor.getTargetEntityType(),
459                 nestedTargetEntityInstances);
460
461             for (JsonNode targetEntityInstance : nestedTargetEntityInstances) {
462               /*
463                * Now: 
464                * 1. build the AAIEntityType (IndexDocument) based on the extract entity 
465                * 2. Get data from ES
466                * 3. Extract ETAG 
467                * 4. Merge ES Doc + AAIEntityType + Extracted Parent Cross-Entity-Reference Values
468                * 5. Put data into ES with ETAG + updated doc 
469                */
470
471               // Get the complete URL for target entity
472               if (targetEntityInstance.has("link")) {   // nested SI has url mentioned
473                 targetEntityUrl = RouterServiceUtil.getNodeFieldAsText(targetEntityInstance, 
474                     "link");
475               } else if ("customer".equalsIgnoreCase(parentEntityType) && 
476                   targetEntityInstance.has("service-instance-id")){
477                 targetEntityUrl += RouterServiceUtil.getNodeFieldAsText(targetEntityInstance, 
478                     "service-instance-id");
479               }
480                     
481               OxmEntityDescriptor searchableDescriptor =
482                   oxmEntities.getSearchableEntityDescriptor(cerDescriptor.getTargetEntityType());
483
484               if (searchableDescriptor != null) {
485
486                 if (!searchableDescriptor.getSearchableAttributes().isEmpty()) {
487
488                   AaiEventEntity entityToSync = null;
489
490                   try {
491
492                     entityToSync = getPopulatedEntity(targetEntityInstance, searchableDescriptor);
493
494                     /*
495                      * Ready to do some ElasticSearch ops
496                      */
497
498                     for (String parentCrossEntityReferenceAttributeValue : extractedParentEntityAttributeValues) {
499                       entityToSync
500                           .addCrossEntityReferenceValue(parentCrossEntityReferenceAttributeValue);
501                     }
502
503                     entityToSync.setLink(targetEntityUrl);
504                     entityToSync.deriveFields();
505
506                     updateCerInEntity(entityToSync);
507
508                   } catch (NoSuchAlgorithmException e) {
509                     logger.debug(e.getMessage());
510                   }
511                 }
512               } else {
513                 logger.debug(EntityEventPolicyMsgs.CROSS_ENTITY_REFERENCE_SYNC,
514                     "failure to find searchable descriptor for type "
515                         + cerDescriptor.getTargetEntityType());
516               }
517             }
518
519           }
520
521         } else {
522           logger.debug(EntityEventPolicyMsgs.CROSS_ENTITY_REFERENCE_SYNC,
523               "failed to find 0 instances of cross-entity-reference with entity " + key);
524         }
525
526       }
527
528     } else {
529       logger.info(EntityEventPolicyMsgs.CROSS_ENTITY_REFERENCE_SYNC, "skipped due to OXM model for "
530           + topEntityType + " does not contain a cross-entity-reference entity");
531     }
532
533     /*
534      * Process for autosuggestable entities
535      */
536     if (oxmEntities != null) {
537       Map<String, OxmEntityDescriptor> rootDescriptor =
538           oxmEntities.getSuggestableEntityDescriptors();
539       if (!rootDescriptor.isEmpty()) {
540         List<String> suggestibleAttrInPayload = new ArrayList<>();
541         List<String> suggestibleAttrInOxm = extractSuggestableAttr(oxmEntities, entityType);
542         if (suggestibleAttrInOxm != null) {
543           for (String attr: suggestibleAttrInOxm){
544             if ( uebObjEntity.has(attr) ){
545               suggestibleAttrInPayload.add(attr);
546             }
547           }
548         }
549
550         if (suggestibleAttrInPayload.isEmpty()) {
551           return;
552         }
553
554         List<String> suggestionAliases = extractAliasForSuggestableEntity(oxmEntities, entityType);
555         AggregationEntity ae = new AggregationEntity();
556         ae.setLink(entityLink);
557         ae.deriveFields(uebAsJson);
558
559         handleSearchServiceOperation(ae, action, aggregateGenericVnfIndex);
560
561         /*
562          * It was decided to silently ignore DELETE requests for resources we don't allow to be
563          * deleted. e.g. auto-suggestion deletion is not allowed while aggregation deletion is.
564          */
565         if (!ACTION_DELETE.equalsIgnoreCase(action)) {
566           List<ArrayList<String>> listOfValidPowerSetElements =
567               SearchSuggestionPermutation.getNonEmptyUniqueLists(suggestibleAttrInPayload);
568
569           // Now we have a list containing the power-set (minus empty element) for the status that are
570           // available in the payload. Try inserting a document for every combination.
571           for (ArrayList<String> list : listOfValidPowerSetElements) {
572             SuggestionSearchEntity suggestionSearchEntity = new SuggestionSearchEntity();
573             suggestionSearchEntity.setEntityType(entityType);
574             suggestionSearchEntity.setSuggestableAttr(list);
575             suggestionSearchEntity.setEntityTypeAliases(suggestionAliases);
576             suggestionSearchEntity.setFilterBasedPayloadFromResponse(uebAsJson.get("entity"),
577                 suggestibleAttrInOxm, list);
578             suggestionSearchEntity.setSuggestionInputPermutations(
579                 suggestionSearchEntity.generateSuggestionInputPermutations());
580
581             if (suggestionSearchEntity.isSuggestableDoc()) {
582               try {
583                 suggestionSearchEntity.generateSearchSuggestionDisplayStringAndId();
584               } catch (NoSuchAlgorithmException e) {
585                 logger.error(EntityEventPolicyMsgs.DISCARD_UPDATING_SEARCH_SUGGESTION_DATA,
586                     "Cannot create unique SHA digest for search suggestion data. Exception: "
587                         + e.getLocalizedMessage());
588               }
589
590               handleSearchServiceOperation(suggestionSearchEntity, action, autosuggestIndex);
591             }
592           }
593         }
594       }
595     }
596
597     long stopTime = System.currentTimeMillis();
598
599     metricsLogger.info(EntityEventPolicyMsgs.OPERATION_RESULT_NO_ERRORS, PROCESS_AAI_EVENT,
600         String.valueOf(stopTime - startTime));
601
602     setResponse(exchange, ResponseType.SUCCESS, additionalInfo);
603     return;
604   }
605
606   public List<String> extractSuggestableAttr(VersionedOxmEntities oxmEntities, String entityType) {
607     // Extract suggestable attributeshandleTopographicalData
608     Map<String, OxmEntityDescriptor> rootDescriptor = oxmEntities.getSuggestableEntityDescriptors();
609
610     if (rootDescriptor == null) {
611       return Collections.emptyList();
612     }
613
614     OxmEntityDescriptor desc = rootDescriptor.get(entityType);
615     
616     if (desc == null) {
617       return Collections.emptyList();
618     }
619
620     return desc.getSuggestableAttributes();
621   }
622
623   public List<String> extractAliasForSuggestableEntity(VersionedOxmEntities oxmEntities,
624       String entityType) {
625
626     // Extract alias
627     Map<String, OxmEntityDescriptor> rootDescriptor = oxmEntities.getEntityAliasDescriptors();
628
629     if (rootDescriptor == null) {
630       return Collections.emptyList();
631     }
632
633     OxmEntityDescriptor desc = rootDescriptor.get(entityType);
634     return desc.getAlias();
635   }
636
637   private void setResponse(Exchange exchange, ResponseType responseType, String additionalInfo) {
638
639     exchange.getOut().setHeader("ResponseType", responseType.toString());
640     exchange.getOut().setBody(additionalInfo);
641   }
642
643   public void extractDetailsForAutosuggestion(VersionedOxmEntities oxmEntities, String entityType,
644       List<String> suggestableAttr, List<String> alias) {
645
646     // Extract suggestable attributes
647     Map<String, OxmEntityDescriptor> rootDescriptor = oxmEntities.getSuggestableEntityDescriptors();
648
649     OxmEntityDescriptor desc = rootDescriptor.get(entityType);
650     suggestableAttr = desc.getSuggestableAttributes();
651
652     // Extract alias
653     rootDescriptor = oxmEntities.getEntityAliasDescriptors();
654     desc = rootDescriptor.get(entityType);
655     alias = desc.getAlias();
656   }
657
658   /*
659    * Load the UEB JSON payload, any errors would result to a failure case response.
660    */
661   private JSONObject getUebContentAsJson(String payload, String contentKey) {
662
663     JSONObject uebJsonObj;
664     JSONObject uebObjContent;
665
666     try {
667       uebJsonObj = new JSONObject(payload);
668     } catch (JSONException e) {
669       logger.debug(EntityEventPolicyMsgs.UEB_INVALID_PAYLOAD_JSON_FORMAT, payload);
670       logger.error(EntityEventPolicyMsgs.UEB_INVALID_PAYLOAD_JSON_FORMAT, payload);
671       return null;
672     }
673
674     if (uebJsonObj.has(contentKey)) {
675       uebObjContent = uebJsonObj.getJSONObject(contentKey);
676     } else {
677       logger.debug(EntityEventPolicyMsgs.UEB_FAILED_TO_PARSE_PAYLOAD, contentKey);
678       logger.error(EntityEventPolicyMsgs.UEB_FAILED_TO_PARSE_PAYLOAD, contentKey);
679       return null;
680     }
681
682     return uebObjContent;
683   }
684
685
686   private UebEventHeader initializeUebEventHeader(String payload) {
687
688     UebEventHeader eventHeader = null;
689     ObjectMapper mapper = new ObjectMapper();
690
691     // Make sure that were were actually passed in a valid string.
692     if (payload == null || payload.isEmpty()) {
693       logger.debug(EntityEventPolicyMsgs.UEB_FAILED_TO_PARSE_PAYLOAD, EVENT_HEADER);
694       logger.error(EntityEventPolicyMsgs.UEB_FAILED_TO_PARSE_PAYLOAD, EVENT_HEADER);
695
696       return eventHeader;
697     }
698
699     // Marshal the supplied string into a UebEventHeader object.
700     try {
701       eventHeader = mapper.readValue(payload, UebEventHeader.class);
702     } catch (JsonProcessingException e) {
703       logger.error(EntityEventPolicyMsgs.UEB_FAILED_UEBEVENTHEADER_CONVERSION, e.toString());
704     } catch (Exception e) {
705       logger.error(EntityEventPolicyMsgs.UEB_FAILED_UEBEVENTHEADER_CONVERSION, e.toString());
706     }
707
708     if (eventHeader != null) {
709       logger.debug(EntityEventPolicyMsgs.UEB_EVENT_HEADER_PARSED, eventHeader.toString());
710     }
711
712     return eventHeader;
713
714   }
715
716
717   private String getEntityPrimaryKeyFieldName(DynamicJAXBContext oxmJaxbContext, String payload,
718       String oxmEntityType, String entityType) {
719
720     DynamicType entity = oxmJaxbContext.getDynamicType(oxmEntityType);
721     if (entity == null) {
722       return null;
723     }
724
725     List<DatabaseField> list = entity.getDescriptor().getPrimaryKeyFields();
726     if (list != null && !list.isEmpty()) {
727       String keyName = list.get(0).getName();
728       return keyName.substring(0, keyName.indexOf('/'));
729     }
730
731     return "";
732   }
733
734   private String lookupValueUsingKey(String payload, String key) throws JSONException {
735     JsonNode jsonNode = convertToJsonNode(payload);
736     return RouterServiceUtil.recursivelyLookupJsonPayload(jsonNode, key);
737   }
738
739   private JsonNode convertToJsonNode(String payload) {
740
741     ObjectMapper mapper = new ObjectMapper();
742     JsonNode jsonNode = null;
743     try {
744       jsonNode = mapper.readTree(mapper.getJsonFactory().createJsonParser(payload));
745     } catch (IOException e) {
746       logger.debug(EntityEventPolicyMsgs.FAILED_TO_PARSE_UEB_PAYLOAD, ENTITY_HEADER + " missing",
747           payload);
748       logger.error(EntityEventPolicyMsgs.FAILED_TO_PARSE_UEB_PAYLOAD, ENTITY_HEADER + " missing",
749           "");
750     }
751
752     return jsonNode;
753   }
754
755   private boolean getSearchTags(AaiEventEntity aaiEventEntity, List<String> searchableAttr,
756       String payload, String action) {
757
758     boolean hasSearchableAttr = false;
759     for (String searchTagField : searchableAttr) {
760       String searchTagValue;
761       if (searchTagField.equalsIgnoreCase(aaiEventEntity.getEntityPrimaryKeyName())) {
762         searchTagValue = aaiEventEntity.getEntityPrimaryKeyValue();
763       } else {
764         searchTagValue = this.lookupValueUsingKey(payload, searchTagField);
765       }
766
767       if (searchTagValue != null && !searchTagValue.isEmpty()) {
768         hasSearchableAttr = true;
769         aaiEventEntity.addSearchTagWithKey(searchTagValue, searchTagField);
770       }
771     }
772     return hasSearchableAttr;
773   }
774
775   /*
776    * Check if OXM version is available. If available, load it.
777    */
778   private DynamicJAXBContext loadOxmContext(String version) {
779     if (version == null) {
780       logger.error(EntityEventPolicyMsgs.FAILED_TO_FIND_OXM_VERSION, version);
781       return null;
782     }
783
784     return oxmVersionContextMap.get(version);
785   }
786
787   private List<String> getOxmAttributes(String payload, DynamicJAXBContext oxmJaxbContext,
788       String oxmEntityType, String entityType, String fieldName) {
789
790     DynamicType entity = (DynamicType) oxmJaxbContext.getDynamicType(oxmEntityType);
791     if (entity == null) {
792       return null;
793     }
794
795     /*
796      * Check for searchable XML tag
797      */
798     List<String> fieldValues = null;
799     Map<String, String> properties = entity.getDescriptor().getProperties();
800     for (Map.Entry<String, String> entry : properties.entrySet()) {
801       if (entry.getKey().equalsIgnoreCase(fieldName)) {
802         fieldValues = Arrays.asList(entry.getValue().split(","));
803         break;
804       }
805     }
806
807     return fieldValues;
808   }
809
810   private JSONObject getUebEntity(String payload) {
811     JSONObject uebJsonObj;
812
813     try {
814       uebJsonObj = new JSONObject(payload);
815     } catch (JSONException e) {
816       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
817           "Payload has invalid JSON Format", payload);
818       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
819           "Payload has invalid JSON Format");
820       return null;
821     }
822
823     if (uebJsonObj.has(ENTITY_HEADER)) {
824       return uebJsonObj.getJSONObject(ENTITY_HEADER);
825     } else {
826       logger.debug(EntityEventPolicyMsgs.FAILED_TO_PARSE_UEB_PAYLOAD, ENTITY_HEADER + " missing", payload);
827       logger.error(EntityEventPolicyMsgs.FAILED_TO_PARSE_UEB_PAYLOAD, ENTITY_HEADER + " missing");
828       return null;
829     }
830   }
831
832   protected AaiEventEntity getPopulatedEntity(JsonNode entityNode,
833       OxmEntityDescriptor resultDescriptor) {
834     AaiEventEntity d = new AaiEventEntity();
835
836     d.setEntityType(resultDescriptor.getEntityName());
837
838     List<String> primaryKeyValues = new ArrayList<>();
839     List<String> primaryKeyNames = new ArrayList<>();
840     String pkeyValue;
841
842     for (String keyName : resultDescriptor.getPrimaryKeyAttributeName()) {
843       pkeyValue = RouterServiceUtil.getNodeFieldAsText(entityNode, keyName);
844       if (pkeyValue != null) {
845         primaryKeyValues.add(pkeyValue);
846         primaryKeyNames.add(keyName);
847       } else {
848         logger.error(EntityEventPolicyMsgs.PRIMARY_KEY_NULL_FOR_ENTITY_TYPE,
849             resultDescriptor.getEntityName());
850       }
851     }
852
853     final String primaryCompositeKeyValue = RouterServiceUtil.concatArray(primaryKeyValues, "/");
854     d.setEntityPrimaryKeyValue(primaryCompositeKeyValue);
855     final String primaryCompositeKeyName = RouterServiceUtil.concatArray(primaryKeyNames, "/");
856     d.setEntityPrimaryKeyName(primaryCompositeKeyName);
857
858     final List<String> searchTagFields = resultDescriptor.getSearchableAttributes();
859
860     /*
861      * Based on configuration, use the configured field names for this entity-Type to build a
862      * multi-value collection of search tags for elastic search entity search criteria.
863      */
864
865
866     for (String searchTagField : searchTagFields) {
867       String searchTagValue = RouterServiceUtil.getNodeFieldAsText(entityNode, searchTagField);
868       if (searchTagValue != null && !searchTagValue.isEmpty()) {
869         d.addSearchTagWithKey(searchTagValue, searchTagField);
870       }
871     }
872
873     return d;
874   }
875
876   private void updateCerInEntity(AaiEventEntity aaiEventEntity) {
877     try {
878       Map<String, List<String>> headers = new HashMap<>();
879       headers.put(Headers.FROM_APP_ID, Arrays.asList("Data Router"));
880       headers.put(Headers.TRANSACTION_ID, Arrays.asList(MDC.get(MdcContext.MDC_REQUEST_ID)));
881
882       String entityId = aaiEventEntity.getId();
883       String jsonPayload;
884
885       // Run the GET to retrieve the ETAG from the search service
886       OperationResult storedEntity = searchAgent.getDocument(entitySearchIndex, entityId);
887
888       if (HttpUtil.isHttpResponseClassSuccess(storedEntity.getResultCode())) {
889         /*
890          * NOTES: aaiEventEntity (ie the nested entity) may contain a subset of properties of 
891          * the pre-existing object, 
892          * so all we want to do is update the CER on the pre-existing object (if needed).
893          */
894         
895         List<String> etag = storedEntity.getHeaders().get(Headers.ETAG);
896
897         if (etag != null && !etag.isEmpty()) {
898           headers.put(Headers.IF_MATCH, etag);
899         } else {
900           logger.error(EntityEventPolicyMsgs.NO_ETAG_AVAILABLE_FAILURE,
901                   entitySearchIndex, entityId);
902         }
903         
904         ArrayList<JsonNode> sourceObject = new ArrayList<>();
905         NodeUtils.extractObjectsByKey(
906             NodeUtils.convertJsonStrToJsonNode(storedEntity.getResult()),
907             "content", sourceObject);
908
909         if (!sourceObject.isEmpty()) {
910           JsonNode node = sourceObject.get(0);
911           final String sourceCer = NodeUtils.extractFieldValueFromObject(node, 
912               "crossReferenceEntityValues");
913           String newCer = aaiEventEntity.getCrossReferenceEntityValues();
914           boolean hasNewCer = true;
915           if (sourceCer != null && sourceCer.length() > 0){ // already has CER
916             if ( !sourceCer.contains(newCer)){//don't re-add
917               newCer = sourceCer + ";" + newCer;
918             } else {
919               hasNewCer = false;
920             }
921           }
922           
923           if (hasNewCer){
924             // Do the PUT with new CER
925             ((ObjectNode)node).put("crossReferenceEntityValues", newCer);
926             jsonPayload = NodeUtils.convertObjectToJson(node, false);
927             searchAgent.putDocument(entitySearchIndex, entityId, jsonPayload, headers);
928           }
929          }
930       } else {
931
932         if (storedEntity.getResultCode() == 404) {
933           // entity not found, so attempt to do a PUT
934           searchAgent.putDocument(entitySearchIndex, entityId, aaiEventEntity.getAsJson(), headers);
935         } else {
936           logger.error(EntityEventPolicyMsgs.FAILED_TO_UPDATE_ENTITY_IN_DOCSTORE,
937               aaiEventEntity.getId(), "SYNC_ENTITY");
938         }
939       }
940     } catch (IOException e) {
941       logger.error(EntityEventPolicyMsgs.FAILED_TO_UPDATE_ENTITY_IN_DOCSTORE,
942           aaiEventEntity.getId(), "SYNC_ENTITY");
943     }
944   }
945
946   /**
947    * Perform create, read, update or delete (CRUD) operation on search engine's suggestive search
948    * index
949    * 
950    * @param eventEntity Entity/data to use in operation
951    * @param action The operation to perform
952    * @param target Resource to perform the operation on
953    * @param allowDeleteEvent Allow delete operation to be performed on resource
954    */
955   protected void handleSearchServiceOperation(DocumentStoreDataEntity eventEntity, 
956                                             String                  action,
957                                             String                  index) {
958     try {
959
960       Map<String, List<String>> headers = new HashMap<>();
961       headers.put(Headers.FROM_APP_ID, Arrays.asList("DataLayer"));
962       headers.put(Headers.TRANSACTION_ID, Arrays.asList(MDC.get(MdcContext.MDC_REQUEST_ID)));
963
964       String entityId = eventEntity.getId();
965
966       if ((action.equalsIgnoreCase(ACTION_CREATE) && entityId != null)
967           || action.equalsIgnoreCase(ACTION_UPDATE)) {
968
969         // Run the GET to retrieve the ETAG from the search service
970         OperationResult storedEntity = searchAgent.getDocument(index, entityId);
971
972         if (HttpUtil.isHttpResponseClassSuccess(storedEntity.getResultCode())) {
973           List<String> etag = storedEntity.getHeaders().get(Headers.ETAG);
974
975           if (etag != null && !etag.isEmpty()) {
976             headers.put(Headers.IF_MATCH, etag);
977           } else {
978             logger.error(EntityEventPolicyMsgs.NO_ETAG_AVAILABLE_FAILURE, index,
979                 entityId);
980           }
981         }
982
983         // Write the entity to the search service.
984         // PUT
985         searchAgent.putDocument(index, entityId, eventEntity.getAsJson(), headers);
986       } else if (action.equalsIgnoreCase(ACTION_CREATE)) {
987         // Write the entry to the search service.
988         searchAgent.postDocument(index, eventEntity.getAsJson(), headers);
989         
990       } else if (action.equalsIgnoreCase(ACTION_DELETE)) {
991         // Run the GET to retrieve the ETAG from the search service
992         OperationResult storedEntity = searchAgent.getDocument(index, entityId);
993
994         if (HttpUtil.isHttpResponseClassSuccess(storedEntity.getResultCode())) {
995           List<String> etag = storedEntity.getHeaders().get(Headers.ETAG);
996
997           if (etag != null && !etag.isEmpty()) {
998             headers.put(Headers.IF_MATCH, etag);
999           } else {
1000             logger.error(EntityEventPolicyMsgs.NO_ETAG_AVAILABLE_FAILURE, index,
1001                 entityId);
1002           }
1003           
1004           /*
1005            * The Spring-Boot version of the search-data-service rejects the DELETE operation unless
1006            * we specify a Content-Type.
1007            */
1008
1009           headers.put("Content-Type", Arrays.asList(MediaType.APPLICATION_JSON.getMediaType()));
1010
1011           searchAgent.deleteDocument(index, eventEntity.getId(), headers);
1012         } else {
1013           logger.error(EntityEventPolicyMsgs.NO_ETAG_AVAILABLE_FAILURE, index,
1014               entityId);
1015         }
1016       } else {
1017         logger.error(EntityEventPolicyMsgs.ENTITY_OPERATION_NOT_SUPPORTED, action);
1018       }
1019     } catch (IOException e) {
1020       logger.error(EntityEventPolicyMsgs.FAILED_TO_UPDATE_ENTITY_IN_DOCSTORE, eventEntity.getId(),
1021           action);
1022     }
1023   }
1024
1025   private void handleTopographicalData(String payload, String action, String entityType,
1026       String oxmEntityType, DynamicJAXBContext oxmJaxbContext, String entityPrimaryKeyFieldName,
1027       String entityPrimaryKeyFieldValue) {
1028
1029     Map<String, String> topoData = new HashMap<>();
1030     String entityLink;
1031     List<String> topographicalAttr =
1032         getOxmAttributes(payload, oxmJaxbContext, oxmEntityType, entityType, "geoProps");
1033     if (topographicalAttr == null) {
1034       logger.error(EntityEventPolicyMsgs.DISCARD_UPDATING_TOPOGRAPHY_DATA_NONVERBOSE,
1035           "Topograhical attribute not found for payload entity type '" + entityType + "'");
1036       logger.debug(EntityEventPolicyMsgs.DISCARD_UPDATING_TOPOGRAPHY_DATA_VERBOSE,
1037           "Topograhical attribute not found for payload entity type '" + entityType + "'",
1038           payload);
1039     } else {
1040       entityLink = lookupValueUsingKey(payload, "entity-link");
1041       for (String topoAttr : topographicalAttr) {
1042         topoData.put(topoAttr, lookupValueUsingKey(payload, topoAttr));
1043       }
1044       updateTopographicalSearchDb(topoData, entityType, action, entityPrimaryKeyFieldName,
1045           entityPrimaryKeyFieldValue, entityLink);
1046     }
1047
1048   }
1049
1050   private void updateTopographicalSearchDb(Map<String, String> topoData, String entityType,
1051       String action, String entityPrimaryKeyName, String entityPrimaryKeyValue, String entityLink) {
1052
1053     TopographicalEntity topoEntity = new TopographicalEntity();
1054     topoEntity.setEntityPrimaryKeyName(entityPrimaryKeyName);
1055     topoEntity.setEntityPrimaryKeyValue(entityPrimaryKeyValue);
1056     topoEntity.setEntityType(entityType);
1057     topoEntity.setLatitude(topoData.get(TOPO_LAT));
1058     topoEntity.setLongitude(topoData.get(TOPO_LONG));
1059     topoEntity.setSelfLink(entityLink);
1060     try {
1061       topoEntity.setId(TopographicalEntity.generateUniqueShaDigest(entityType, entityPrimaryKeyName,
1062           entityPrimaryKeyValue));
1063     } catch (NoSuchAlgorithmException e) {
1064       logger.error(EntityEventPolicyMsgs.DISCARD_UPDATING_TOPOGRAPHY_DATA_VERBOSE,
1065           "Cannot create unique SHA digest for topographical data.");
1066     }
1067
1068     this.handleSearchServiceOperation(topoEntity, action, topographicalSearchIndex);
1069   }
1070
1071
1072   // put this here until we find a better spot
1073   /**
1074    * Helper utility to concatenate substrings of a URI together to form a proper URI.
1075    * 
1076    * @param suburis the list of substrings to concatenate together
1077    * @return the concatenated list of substrings
1078    */
1079   public static String concatSubUri(String... suburis) {
1080     String finalUri = "";
1081
1082     for (String suburi : suburis) {
1083
1084       if (suburi != null) {
1085         // Remove any leading / since we only want to append /
1086         suburi = suburi.replaceFirst("^/*", "");
1087
1088         // Add a trailing / if one isn't already there
1089         finalUri += suburi.endsWith("/") ? suburi : suburi + "/";
1090       }
1091     }
1092
1093     return finalUri;
1094   }
1095 }