Update license date and text
[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  * ================================================================================
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 package org.onap.aai.datarouter.policy;
22
23 import java.io.FileNotFoundException;
24 import java.io.IOException;
25 import java.security.NoSuchAlgorithmException;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.HashMap;
31 import java.util.Iterator;
32 import java.util.List;
33 import java.util.Map;
34
35 import org.apache.camel.Exchange;
36 import org.apache.camel.Processor;
37 import org.eclipse.persistence.dynamic.DynamicType;
38 import org.eclipse.persistence.internal.helper.DatabaseField;
39 import org.eclipse.persistence.jaxb.dynamic.DynamicJAXBContext;
40 import org.json.JSONException;
41 import org.json.JSONObject;
42 import org.onap.aai.datarouter.entity.AaiEventEntity;
43 import org.onap.aai.datarouter.entity.AggregationEntity;
44 import org.onap.aai.datarouter.entity.DocumentStoreDataEntity;
45 import org.onap.aai.datarouter.entity.OxmEntityDescriptor;
46 import org.onap.aai.datarouter.entity.SuggestionSearchEntity;
47 import org.onap.aai.datarouter.entity.TopographicalEntity;
48 import org.onap.aai.datarouter.entity.UebEventHeader;
49 import org.onap.aai.datarouter.logging.EntityEventPolicyMsgs;
50 import org.onap.aai.datarouter.util.CrossEntityReference;
51 import org.onap.aai.datarouter.util.EntityOxmReferenceHelper;
52 import org.onap.aai.datarouter.util.ExternalOxmModelProcessor;
53 import org.onap.aai.datarouter.util.NodeUtils;
54 import org.onap.aai.datarouter.util.OxmModelLoader;
55 import org.onap.aai.datarouter.util.RouterServiceUtil;
56 import org.onap.aai.datarouter.util.SearchServiceAgent;
57 import org.onap.aai.datarouter.util.SearchSuggestionPermutation;
58 import org.onap.aai.datarouter.util.Version;
59 import org.onap.aai.datarouter.util.VersionedOxmEntities;
60 import org.onap.aai.cl.api.Logger;
61 import org.onap.aai.cl.eelf.LoggerFactory;
62 import org.onap.aai.cl.mdc.MdcContext;
63 import org.onap.aai.restclient.client.Headers;
64 import org.onap.aai.restclient.client.OperationResult;
65 import org.onap.aai.restclient.rest.HttpUtil;
66 import org.slf4j.MDC;
67
68 import com.fasterxml.jackson.core.JsonProcessingException;
69 import com.fasterxml.jackson.databind.JsonNode;
70 import com.fasterxml.jackson.databind.ObjectMapper;
71 import com.fasterxml.jackson.databind.ObjectWriter;
72 import com.fasterxml.jackson.databind.node.ObjectNode;
73
74 public class EntityEventPolicy implements Processor {
75
76   public static final String additionalInfo = "Response of AAIEntityEventPolicy";
77   private static final String entitySearchSchema = "entitysearch_schema.json";
78   private static final String topographicalSearchSchema   = "topographysearch_schema.json";
79   private Collection<ExternalOxmModelProcessor> externalOxmModelProcessors;
80
81   private final String EVENT_HEADER = "event-header";
82   private final String ENTITY_HEADER = "entity";
83   private final String ACTION_CREATE = "create";
84   private final static String ACTION_DELETE = "delete";
85   private final String ACTION_UPDATE = "update";
86   private final String PROCESS_AAI_EVENT = "Process AAI Event";
87   private final String TOPO_LAT = "latitude";
88   private final String TOPO_LONG = "longitude";
89
90   private final List<String> SUPPORTED_ACTIONS =
91       Arrays.asList(ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE);
92
93   Map<String, DynamicJAXBContext> oxmVersionContextMap = new HashMap<>();
94   private String oxmVersion = null;
95
96   /** Agent for communicating with the Search Service. */
97   private SearchServiceAgent searchAgent = null;
98   
99   /** Search index name for storing AAI event entities. */
100   private String entitySearchIndex;
101
102   /** Search index name for storing topographical search data. */
103   private String topographicalSearchIndex;
104   
105   /** Search index name for suggestive search data. */
106   private String aggregateGenericVnfIndex;
107   
108   private String autosuggestIndex;
109
110   private String srcDomain;
111
112   private Logger logger;
113   private Logger metricsLogger;
114
115   public enum ResponseType {
116     SUCCESS, PARTIAL_SUCCESS, FAILURE;
117   };
118
119   public EntityEventPolicy(EntityEventPolicyConfig config) throws FileNotFoundException {
120     LoggerFactory loggerFactoryInstance = LoggerFactory.getInstance();
121     logger = loggerFactoryInstance.getLogger(EntityEventPolicy.class.getName());
122     metricsLogger = loggerFactoryInstance.getMetricsLogger(EntityEventPolicy.class.getName());
123
124
125     srcDomain = config.getSourceDomain();
126
127     // Populate the index names.
128     entitySearchIndex        = config.getSearchEntitySearchIndex();
129     topographicalSearchIndex = config.getSearchTopographySearchIndex();
130     aggregateGenericVnfIndex = config.getSearchAggregationVnfIndex();
131     autosuggestIndex             = config.getSearchEntityAutoSuggestIndex();
132        
133     // Instantiate the agent that we will use for interacting with the Search Service.
134     searchAgent = new SearchServiceAgent(config.getSearchCertName(),
135                                          config.getSearchKeystore(),
136                                          config.getSearchKeystorePwd(),
137                                          EntityEventPolicy.concatSubUri(config.getSearchBaseUrl(),
138                                                                         config.getSearchEndpoint()),
139                                          config.getSearchEndpointDocuments(),
140                                          logger);
141
142     this.externalOxmModelProcessors = new ArrayList<>();
143     this.externalOxmModelProcessors.add(EntityOxmReferenceHelper.getInstance());
144     OxmModelLoader.registerExternalOxmModelProcessors(externalOxmModelProcessors);
145     OxmModelLoader.loadModels();
146     oxmVersionContextMap = OxmModelLoader.getVersionContextMap();
147     parseLatestOxmVersion();
148   }
149
150   private void parseLatestOxmVersion() {
151     int latestVersion = -1;
152     if (oxmVersionContextMap != null) {
153       Iterator it = oxmVersionContextMap.entrySet().iterator();
154       while (it.hasNext()) {
155         Map.Entry pair = (Map.Entry) it.next();
156
157         String version = pair.getKey().toString();
158         int versionNum = Integer.parseInt(version.substring(1, version.length()));
159
160         if (versionNum > latestVersion) {
161           latestVersion = versionNum;
162           oxmVersion = pair.getKey().toString();
163         }
164
165         logger.info(EntityEventPolicyMsgs.PROCESS_OXM_MODEL_FOUND, pair.getKey().toString());
166       }
167     } else {
168       logger.error(EntityEventPolicyMsgs.PROCESS_OXM_MODEL_MISSING, "");
169     }
170   }
171
172   public void startup() {
173     
174     // Create the indexes in the search service if they do not already exist.
175     searchAgent.createSearchIndex(entitySearchIndex, entitySearchSchema);
176     searchAgent.createSearchIndex(topographicalSearchIndex, topographicalSearchSchema);
177     
178     logger.info(EntityEventPolicyMsgs.ENTITY_EVENT_POLICY_REGISTERED);
179   }
180
181
182   /**
183    * Convert object to json.
184    *
185    * @param object the object
186    * @param pretty the pretty
187    * @return the string
188    * @throws JsonProcessingException the json processing exception
189    */
190   public static String convertObjectToJson(Object object, boolean pretty)
191       throws JsonProcessingException {
192     ObjectWriter ow;
193
194     if (pretty) {
195       ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
196
197     } else {
198       ow = new ObjectMapper().writer();
199     }
200
201     return ow.writeValueAsString(object);
202   }
203
204   public void returnWithError(Exchange exchange, String payload, String errorMsg){
205     logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE, errorMsg);
206     logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE, errorMsg, payload);
207     setResponse(exchange, ResponseType.FAILURE, additionalInfo);
208   }
209   
210   @Override
211   public void process(Exchange exchange) throws Exception {
212
213     long startTime = System.currentTimeMillis();
214
215     String uebPayload = exchange.getIn().getBody().toString();
216
217     JsonNode uebAsJson =null;
218     ObjectMapper mapper = new ObjectMapper();
219     try{
220       uebAsJson = mapper.readTree(uebPayload);
221     } catch (IOException e){
222       returnWithError(exchange, uebPayload, "Invalid Payload");
223       return;
224     }
225
226     // Load the UEB payload data, any errors will result in a failure and discard
227     JSONObject uebObjHeader = getUebContentAsJson(uebPayload, EVENT_HEADER);
228     if (uebObjHeader == null) {
229       returnWithError(exchange, uebPayload, "Payload is missing " + EVENT_HEADER);
230       return;
231     }
232     
233     JSONObject uebObjEntity = getUebContentAsJson(uebPayload, ENTITY_HEADER);
234     if (uebObjEntity == null) {
235       returnWithError(exchange, uebPayload, "Payload is missing " + ENTITY_HEADER);
236       return;
237     }
238
239     UebEventHeader eventHeader;
240     eventHeader = initializeUebEventHeader(uebObjHeader.toString());
241
242     // Get src domain from header; discard event if not originated from same domain
243     String payloadSrcDomain = eventHeader.getDomain();
244     if (payloadSrcDomain == null || !payloadSrcDomain.equalsIgnoreCase(this.srcDomain)) {
245       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
246           "Unrecognized source domain '" + payloadSrcDomain + "'", uebPayload);
247       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
248           "Unrecognized source domain '" + payloadSrcDomain + "'");
249
250       setResponse(exchange, ResponseType.SUCCESS, additionalInfo);
251       return;
252     }
253
254     DynamicJAXBContext oxmJaxbContext = loadOxmContext(oxmVersion.toLowerCase());
255     if (oxmJaxbContext == null) {
256       logger.error(EntityEventPolicyMsgs.OXM_VERSION_NOT_SUPPORTED, oxmVersion);
257       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE, "OXM version mismatch",
258           uebPayload);
259
260       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
261       return;
262     }
263
264     String action = eventHeader.getAction();
265     if (action == null || !SUPPORTED_ACTIONS.contains(action.toLowerCase())) {
266       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
267           "Unrecognized action '" + action + "'", uebPayload);
268       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
269           "Unrecognized action '" + action + "'");
270
271       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
272       return;
273     }
274
275     String entityType = eventHeader.getEntityType();
276     if (entityType == null) {
277       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
278           "Payload header missing entity type", uebPayload);
279       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
280           "Payload header missing entity type");
281
282       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
283       return;
284     }
285
286     String topEntityType = eventHeader.getTopEntityType();
287     if (topEntityType == null) {
288       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
289           "Payload header missing top entity type", uebPayload);
290       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
291           "Payload header top missing entity type");
292
293       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
294       return;
295     }
296
297     String entityLink = eventHeader.getEntityLink();
298     if (entityLink == null) {
299       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
300           "Payload header missing entity link", uebPayload);
301       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
302           "Payload header missing entity link");
303
304       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
305       return;
306     }
307
308     // log the fact that all data are in good shape
309     logger.info(EntityEventPolicyMsgs.PROCESS_ENTITY_EVENT_POLICY_NONVERBOSE, action,
310         entityType);
311     logger.debug(EntityEventPolicyMsgs.PROCESS_ENTITY_EVENT_POLICY_VERBOSE, action, entityType,
312         uebPayload);
313
314
315     // Process for building AaiEventEntity object
316     String[] entityTypeArr = entityType.split("-");
317     String oxmEntityType = "";
318     for (String entityWord : entityTypeArr) {
319       oxmEntityType += entityWord.substring(0, 1).toUpperCase() + entityWord.substring(1);
320     }
321
322     List<String> searchableAttr =
323         getOxmAttributes(uebPayload, oxmJaxbContext, oxmEntityType, entityType, "searchable");
324     if (searchableAttr == null) {
325       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
326           "Searchable attribute not found for payload entity type '" + entityType + "'");
327       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
328           "Searchable attribute not found for payload entity type '" + entityType + "'",
329           uebPayload);
330
331       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
332       return;
333     }
334
335     String entityPrimaryKeyFieldName =
336         getEntityPrimaryKeyFieldName(oxmJaxbContext, uebPayload, oxmEntityType, entityType);
337     String entityPrimaryKeyFieldValue = lookupValueUsingKey(uebPayload, entityPrimaryKeyFieldName);
338     if (entityPrimaryKeyFieldValue == null || entityPrimaryKeyFieldValue.isEmpty()) {
339       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
340           "Payload missing primary key attribute");
341       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
342           "Payload missing primary key attribute", uebPayload);
343
344       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
345       return;
346     }
347
348     AaiEventEntity aaiEventEntity = new AaiEventEntity();
349
350     /*
351      * Use the OXM Model to determine the primary key field name based on the entity-type
352      */
353
354     aaiEventEntity.setEntityPrimaryKeyName(entityPrimaryKeyFieldName);
355     aaiEventEntity.setEntityPrimaryKeyValue(entityPrimaryKeyFieldValue);
356     aaiEventEntity.setEntityType(entityType);
357     aaiEventEntity.setLink(entityLink);
358
359     if (!getSearchTags(aaiEventEntity, searchableAttr, uebPayload, action)) {
360       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_NONVERBOSE,
361           "Payload missing searchable attribute for entity type '" + entityType + "'");
362       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
363           "Payload missing searchable attribute for entity type '" + entityType + "'", uebPayload);
364
365       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
366       return;
367
368     }
369
370     try {
371       aaiEventEntity.deriveFields();
372
373     } catch (NoSuchAlgorithmException e) {
374       logger.error(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
375           "Cannot create unique SHA digest");
376       logger.debug(EntityEventPolicyMsgs.DISCARD_EVENT_VERBOSE,
377           "Cannot create unique SHA digest", uebPayload);
378
379       setResponse(exchange, ResponseType.FAILURE, additionalInfo);
380       return;
381     }
382
383     handleSearchServiceOperation(aaiEventEntity, action, entitySearchIndex);
384
385     handleTopographicalData(uebPayload, action, entityType, oxmEntityType, oxmJaxbContext,
386         entityPrimaryKeyFieldName, entityPrimaryKeyFieldValue);
387
388     /*
389      * Use the versioned OXM Entity class to get access to cross-entity reference helper collections
390      */
391     VersionedOxmEntities oxmEntities =
392         EntityOxmReferenceHelper.getInstance().getVersionedOxmEntities(Version.valueOf(oxmVersion));
393
394     /**
395      * NOTES:
396      * 1. If the entity type is "customer", the below check will return true if any nested entityType
397      * in that model could contain a CER based on the OXM model version that has been loaded.
398      * 2. For a DELETE operation on outer/parent entity (handled by the regular flow: 
399      * handleSearchServiceOperation()), ignore processing for cross-entity-reference under the
400      * assumption that AAI will push down all required cascade-deletes for nested entities as well
401      * 3. Handling the case where UEB events arrive out of order: CREATE customer is received before
402      *  CREATE service-instance.
403      */
404
405     if (!action.equalsIgnoreCase(ACTION_DELETE) && oxmEntities != null 
406         && oxmEntities.entityModelContainsCrossEntityReference(topEntityType)) {
407
408       // We know the model "can" contain a CER reference definition, let's process a bit more
409
410       HashMap<String, CrossEntityReference> crossEntityRefMap =
411           oxmEntities.getCrossEntityReferences();
412
413       JSONObject entityJsonObject = getUebEntity(uebPayload);
414
415       JsonNode entityJsonNode = convertToJsonNode(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                     e.printStackTrace();
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.toString());
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",
827           payload.toString());
828       logger.error(EntityEventPolicyMsgs.FAILED_TO_PARSE_UEB_PAYLOAD, ENTITY_HEADER + " missing");
829       return null;
830     }
831   }
832
833   protected AaiEventEntity getPopulatedEntity(JsonNode entityNode,
834       OxmEntityDescriptor resultDescriptor) {
835     AaiEventEntity d = new AaiEventEntity();
836
837     d.setEntityType(resultDescriptor.getEntityName());
838
839     List<String> primaryKeyValues = new ArrayList<>();
840     List<String> primaryKeyNames = new ArrayList<>();
841     String pkeyValue;
842
843     for (String keyName : resultDescriptor.getPrimaryKeyAttributeName()) {
844       pkeyValue = RouterServiceUtil.getNodeFieldAsText(entityNode, keyName);
845       if (pkeyValue != null) {
846         primaryKeyValues.add(pkeyValue);
847         primaryKeyNames.add(keyName);
848       } else {
849         // logger.warn("getPopulatedDocument(), pKeyValue is null for entityType = " +
850         // resultDescriptor.getEntityName());
851         logger.error(EntityEventPolicyMsgs.PRIMARY_KEY_NULL_FOR_ENTITY_TYPE,
852             resultDescriptor.getEntityName());
853       }
854     }
855
856     final String primaryCompositeKeyValue = RouterServiceUtil.concatArray(primaryKeyValues, "/");
857     d.setEntityPrimaryKeyValue(primaryCompositeKeyValue);
858     final String primaryCompositeKeyName = RouterServiceUtil.concatArray(primaryKeyNames, "/");
859     d.setEntityPrimaryKeyName(primaryCompositeKeyName);
860
861     final List<String> searchTagFields = resultDescriptor.getSearchableAttributes();
862
863     /*
864      * Based on configuration, use the configured field names for this entity-Type to build a
865      * multi-value collection of search tags for elastic search entity search criteria.
866      */
867
868
869     for (String searchTagField : searchTagFields) {
870       String searchTagValue = RouterServiceUtil.getNodeFieldAsText(entityNode, searchTagField);
871       if (searchTagValue != null && !searchTagValue.isEmpty()) {
872         d.addSearchTagWithKey(searchTagValue, searchTagField);
873       }
874     }
875
876     return d;
877   }
878
879   private void updateCerInEntity(AaiEventEntity aaiEventEntity) {
880     try {
881       Map<String, List<String>> headers = new HashMap<>();
882       headers.put(Headers.FROM_APP_ID, Arrays.asList("Data Router"));
883       headers.put(Headers.TRANSACTION_ID, Arrays.asList(MDC.get(MdcContext.MDC_REQUEST_ID)));
884
885       String entityId = aaiEventEntity.getId();
886       String jsonPayload;
887
888       // Run the GET to retrieve the ETAG from the search service
889       OperationResult storedEntity = searchAgent.getDocument(entitySearchIndex, entityId);
890
891       if (HttpUtil.isHttpResponseClassSuccess(storedEntity.getResultCode())) {
892         /*
893          * NOTES: aaiEventEntity (ie the nested entity) may contain a subset of properties of 
894          * the pre-existing object, 
895          * so all we want to do is update the CER on the pre-existing object (if needed).
896          */
897         
898         List<String> etag = storedEntity.getHeaders().get(Headers.ETAG);
899
900         if (etag != null && !etag.isEmpty()) {
901           headers.put(Headers.IF_MATCH, etag);
902         } else {
903           logger.error(EntityEventPolicyMsgs.NO_ETAG_AVAILABLE_FAILURE,
904                           entitySearchIndex, entityId);
905         }
906         
907         ArrayList<JsonNode> sourceObject = new ArrayList<>();
908         NodeUtils.extractObjectsByKey(
909             NodeUtils.convertJsonStrToJsonNode(storedEntity.getResult()),
910             "content", sourceObject);
911
912         if (!sourceObject.isEmpty()) {
913           JsonNode node = sourceObject.get(0);
914           final String sourceCer = NodeUtils.extractFieldValueFromObject(node, 
915               "crossEntityReferenceValues");
916           String newCer = aaiEventEntity.getCrossReferenceEntityValues();
917           boolean hasNewCer = true;
918           if (sourceCer != null && sourceCer.length() > 0){ // already has CER
919             if ( !sourceCer.contains(newCer)){//don't re-add
920               newCer = sourceCer + ";" + newCer;
921             } else {
922               hasNewCer = false;
923             }
924           }
925           
926           if (hasNewCer){
927             // Do the PUT with new CER
928             ((ObjectNode)node).put("crossEntityReferenceValues", newCer);
929             jsonPayload = NodeUtils.convertObjectToJson(node, false);
930             searchAgent.putDocument(entitySearchIndex, entityId, jsonPayload, headers);
931           }
932          }
933       } else {
934
935         if (storedEntity.getResultCode() == 404) {
936           // entity not found, so attempt to do a PUT
937           searchAgent.putDocument(entitySearchIndex, entityId, aaiEventEntity.getAsJson(), headers);
938         } else {
939           logger.error(EntityEventPolicyMsgs.FAILED_TO_UPDATE_ENTITY_IN_DOCSTORE,
940               aaiEventEntity.getId(), "SYNC_ENTITY");
941         }
942       }
943     } catch (IOException e) {
944       logger.error(EntityEventPolicyMsgs.FAILED_TO_UPDATE_ENTITY_IN_DOCSTORE,
945           aaiEventEntity.getId(), "SYNC_ENTITY");
946     }
947   }
948
949   /**
950    * Perform create, read, update or delete (CRUD) operation on search engine's suggestive search
951    * index
952    * 
953    * @param eventEntity Entity/data to use in operation
954    * @param action The operation to perform
955    * @param target Resource to perform the operation on
956    * @param allowDeleteEvent Allow delete operation to be performed on resource
957    */
958   protected void handleSearchServiceOperation(DocumentStoreDataEntity eventEntity, 
959                                             String                  action,
960                                             String                  index) {
961     try {
962
963       Map<String, List<String>> headers = new HashMap<>();
964       headers.put(Headers.FROM_APP_ID, Arrays.asList("DataLayer"));
965       headers.put(Headers.TRANSACTION_ID, Arrays.asList(MDC.get(MdcContext.MDC_REQUEST_ID)));
966
967       String entityId = eventEntity.getId();
968
969       // System.out.println("aaiEventEntity as json = " + aaiEventEntity.getAsJson());
970
971       if ((action.equalsIgnoreCase(ACTION_CREATE) && entityId != null)
972           || action.equalsIgnoreCase(ACTION_UPDATE)) {
973
974         // Run the GET to retrieve the ETAG from the search service
975         OperationResult storedEntity = searchAgent.getDocument(index, entityId);
976
977         if (HttpUtil.isHttpResponseClassSuccess(storedEntity.getResultCode())) {
978           List<String> etag = storedEntity.getHeaders().get(Headers.ETAG);
979
980           if (etag != null && !etag.isEmpty()) {
981             headers.put(Headers.IF_MATCH, etag);
982           } else {
983             logger.error(EntityEventPolicyMsgs.NO_ETAG_AVAILABLE_FAILURE, index,
984                 entityId);
985           }
986         }
987
988         // Write the entity to the search service.
989         // PUT
990         searchAgent.putDocument(index, entityId, eventEntity.getAsJson(), headers);
991       } else if (action.equalsIgnoreCase(ACTION_CREATE)) {
992         // Write the entry to the search service.
993         searchAgent.postDocument(index, eventEntity.getAsJson(), headers);
994         
995       } else if (action.equalsIgnoreCase(ACTION_DELETE)) {
996         // Run the GET to retrieve the ETAG from the search service
997         OperationResult storedEntity = searchAgent.getDocument(index, entityId);
998
999         if (HttpUtil.isHttpResponseClassSuccess(storedEntity.getResultCode())) {
1000           List<String> etag = storedEntity.getHeaders().get(Headers.ETAG);
1001
1002           if (etag != null && !etag.isEmpty()) {
1003             headers.put(Headers.IF_MATCH, etag);
1004           } else {
1005             logger.error(EntityEventPolicyMsgs.NO_ETAG_AVAILABLE_FAILURE, index,
1006                 entityId);
1007           }
1008
1009           searchAgent.deleteDocument(index, eventEntity.getId(), headers);
1010         } else {
1011           logger.error(EntityEventPolicyMsgs.NO_ETAG_AVAILABLE_FAILURE, index,
1012               entityId);
1013         }
1014       } else {
1015         logger.error(EntityEventPolicyMsgs.ENTITY_OPERATION_NOT_SUPPORTED, action);
1016       }
1017     } catch (IOException e) {
1018       logger.error(EntityEventPolicyMsgs.FAILED_TO_UPDATE_ENTITY_IN_DOCSTORE, eventEntity.getId(),
1019           action);
1020     }
1021   }
1022
1023   private void handleTopographicalData(String payload, String action, String entityType,
1024       String oxmEntityType, DynamicJAXBContext oxmJaxbContext, String entityPrimaryKeyFieldName,
1025       String entityPrimaryKeyFieldValue) {
1026
1027     Map<String, String> topoData = new HashMap<>();
1028     String entityLink;
1029     List<String> topographicalAttr =
1030         getOxmAttributes(payload, oxmJaxbContext, oxmEntityType, entityType, "geoProps");
1031     if (topographicalAttr == null) {
1032       logger.error(EntityEventPolicyMsgs.DISCARD_UPDATING_TOPOGRAPHY_DATA_NONVERBOSE,
1033           "Topograhical attribute not found for payload entity type '" + entityType + "'");
1034       logger.debug(EntityEventPolicyMsgs.DISCARD_UPDATING_TOPOGRAPHY_DATA_VERBOSE,
1035           "Topograhical attribute not found for payload entity type '" + entityType + "'",
1036           payload);
1037     } else {
1038       entityLink = lookupValueUsingKey(payload, "entity-link");
1039       for (String topoAttr : topographicalAttr) {
1040         topoData.put(topoAttr, lookupValueUsingKey(payload, topoAttr));
1041       }
1042       updateTopographicalSearchDb(topoData, entityType, action, entityPrimaryKeyFieldName,
1043           entityPrimaryKeyFieldValue, entityLink);
1044     }
1045
1046   }
1047
1048   private void updateTopographicalSearchDb(Map<String, String> topoData, String entityType,
1049       String action, String entityPrimaryKeyName, String entityPrimaryKeyValue, String entityLink) {
1050
1051     TopographicalEntity topoEntity = new TopographicalEntity();
1052     topoEntity.setEntityPrimaryKeyName(entityPrimaryKeyName);
1053     topoEntity.setEntityPrimaryKeyValue(entityPrimaryKeyValue);
1054     topoEntity.setEntityType(entityType);
1055     topoEntity.setLatitude(topoData.get(TOPO_LAT));
1056     topoEntity.setLongitude(topoData.get(TOPO_LONG));
1057     topoEntity.setSelfLink(entityLink);
1058     try {
1059       topoEntity.setId(TopographicalEntity.generateUniqueShaDigest(entityType, entityPrimaryKeyName,
1060           entityPrimaryKeyValue));
1061     } catch (NoSuchAlgorithmException e) {
1062       logger.error(EntityEventPolicyMsgs.DISCARD_UPDATING_TOPOGRAPHY_DATA_VERBOSE,
1063           "Cannot create unique SHA digest for topographical data.");
1064     }
1065
1066     this.handleSearchServiceOperation(topoEntity, action, topographicalSearchIndex);
1067   }
1068
1069
1070   // put this here until we find a better spot
1071   /**
1072    * Helper utility to concatenate substrings of a URI together to form a proper URI.
1073    * 
1074    * @param suburis the list of substrings to concatenate together
1075    * @return the concatenated list of substrings
1076    */
1077   public static String concatSubUri(String... suburis) {
1078     String finalUri = "";
1079
1080     for (String suburi : suburis) {
1081
1082       if (suburi != null) {
1083         // Remove any leading / since we only want to append /
1084         suburi = suburi.replaceFirst("^/*", "");
1085
1086         // Add a trailing / if one isn't already there
1087         finalUri += suburi.endsWith("/") ? suburi : suburi + "/";
1088       }
1089     }
1090
1091     return finalUri;
1092   }
1093 }