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