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