Update Sparky README files
[aai/sparky-be.git] / src / main / java / org / onap / aai / sparky / viewandinspect / services / BaseVisualizationContext.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.sparky.viewandinspect.services;
24
25 import static java.util.concurrent.CompletableFuture.supplyAsync;
26
27 import java.net.URISyntaxException;
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.Iterator;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Map.Entry;
34 import java.util.concurrent.ConcurrentHashMap;
35 import java.util.concurrent.ExecutorService;
36 import java.util.concurrent.atomic.AtomicInteger;
37
38 import org.apache.http.client.utils.URIBuilder;
39 import org.onap.aai.cl.api.Logger;
40 import org.onap.aai.cl.eelf.LoggerFactory;
41 import org.onap.aai.restclient.client.OperationResult;
42 import org.onap.aai.sparky.config.oxm.OxmEntityDescriptor;
43 import org.onap.aai.sparky.config.oxm.OxmEntityLookup;
44 import org.onap.aai.sparky.dal.ActiveInventoryAdapter;
45 import org.onap.aai.sparky.logging.AaiUiMsgs;
46 import org.onap.aai.sparky.sync.entity.SearchableEntity;
47 import org.onap.aai.sparky.util.NodeUtils;
48 import org.onap.aai.sparky.viewandinspect.config.SparkyConstants;
49 import org.onap.aai.sparky.viewandinspect.config.VisualizationConfigs;
50 import org.onap.aai.sparky.viewandinspect.entity.ActiveInventoryNode;
51 import org.onap.aai.sparky.viewandinspect.entity.InlineMessage;
52 import org.onap.aai.sparky.viewandinspect.entity.NodeProcessingTransaction;
53 import org.onap.aai.sparky.viewandinspect.entity.QueryParams;
54 import org.onap.aai.sparky.viewandinspect.entity.Relationship;
55 import org.onap.aai.sparky.viewandinspect.entity.RelationshipData;
56 import org.onap.aai.sparky.viewandinspect.entity.RelationshipList;
57 import org.onap.aai.sparky.viewandinspect.entity.SelfLinkDeterminationTransaction;
58 import org.onap.aai.sparky.viewandinspect.enumeration.NodeProcessingAction;
59 import org.onap.aai.sparky.viewandinspect.enumeration.NodeProcessingState;
60 import org.onap.aai.sparky.viewandinspect.task.PerformNodeSelfLinkProcessingTask;
61 import org.onap.aai.sparky.viewandinspect.task.PerformSelfLinkDeterminationTask;
62
63 import com.fasterxml.jackson.annotation.JsonInclude.Include;
64 import com.fasterxml.jackson.databind.JsonNode;
65 import com.fasterxml.jackson.databind.ObjectMapper;
66 import com.fasterxml.jackson.databind.PropertyNamingStrategy;
67
68 /** 
69  * The Class SelfLinkNodeCollector.
70  */
71 public class BaseVisualizationContext implements VisualizationContext {
72
73   private static final int MAX_DEPTH_EVALUATION_ATTEMPTS = 100;
74   private static final String DEPTH_ALL_MODIFIER = "?depth=all";
75   private static final String NODES_ONLY_MODIFIER = "?nodes-only";
76   private static final String SERVICE_INSTANCE = "service-instance";
77
78   private static final Logger LOG = LoggerFactory.getInstance().getLogger(
79       BaseVisualizationContext.class);
80   private final ActiveInventoryAdapter aaiAdapter;
81
82   private int maxSelfLinkTraversalDepth;
83   private AtomicInteger numLinksDiscovered;
84   private AtomicInteger numSuccessfulLinkResolveFromCache;
85   private AtomicInteger numSuccessfulLinkResolveFromFromServer;
86   private AtomicInteger numFailedLinkResolve;
87   private AtomicInteger aaiWorkOnHand;
88  
89   private VisualizationConfigs visualizationConfigs;
90
91   private AtomicInteger totalLinksRetrieved;
92
93   private final long contextId;
94   private final String contextIdStr;
95
96   private ObjectMapper mapper;
97   private InlineMessage inlineMessage = null;
98   
99   private ExecutorService aaiExecutorService;
100   private OxmEntityLookup oxmEntityLookup;
101
102   /*
103    * The node cache is intended to be a flat structure indexed by a primary key to avoid needlessly
104    * re-requesting the same self-links over-and-over again, to speed up the overall render time and
105    * more importantly to reduce the network cost of determining information we already have.
106    */
107   private ConcurrentHashMap<String, ActiveInventoryNode> nodeCache;
108
109   /**
110    * Instantiates a new self link node collector.
111    *
112    * @param loader the loader
113    * @throws Exception the exception
114    */
115   public BaseVisualizationContext(long contextId, ActiveInventoryAdapter aaiAdapter,
116       ExecutorService aaiExecutorService, VisualizationConfigs visualizationConfigs,
117       OxmEntityLookup oxmEntityLookup)
118       throws Exception {
119     
120     this.contextId = contextId;
121     this.contextIdStr = "[Context-Id=" + contextId + "]";
122     this.aaiAdapter = aaiAdapter;
123     this.aaiExecutorService = aaiExecutorService;
124     this.visualizationConfigs = visualizationConfigs;
125     this.oxmEntityLookup = oxmEntityLookup;
126     
127     this.nodeCache = new ConcurrentHashMap<String, ActiveInventoryNode>();
128     this.numLinksDiscovered = new AtomicInteger(0);
129     this.totalLinksRetrieved = new AtomicInteger(0);
130     this.numSuccessfulLinkResolveFromCache = new AtomicInteger(0);
131     this.numSuccessfulLinkResolveFromFromServer = new AtomicInteger(0);
132     this.numFailedLinkResolve = new AtomicInteger(0);
133     this.aaiWorkOnHand = new AtomicInteger(0);
134
135     this.maxSelfLinkTraversalDepth = this.visualizationConfigs.getMaxSelfLinkTraversalDepth();
136
137     this.mapper = new ObjectMapper();
138     mapper.setSerializationInclusion(Include.NON_EMPTY);
139     mapper.setPropertyNamingStrategy(new PropertyNamingStrategy.KebabCaseStrategy());
140   }
141   
142   public long getContextId() {
143     return contextId;
144   }
145
146   /**
147    * A utility method for extracting all entity-type primary key values from a provided self-link
148    * and return a set of generic-query API keys.
149    * 
150    * @param parentEntityType
151    * @param link
152    * @return a list of key values that can be used for this entity with the AAI generic-query API
153    */
154   protected List<String> extractQueryParamsFromSelfLink(String link) {
155
156     List<String> queryParams = new ArrayList<String>();
157
158     if (link == null) {
159       LOG.error(AaiUiMsgs.QUERY_PARAM_EXTRACTION_ERROR, "self link is null");
160       return queryParams;
161     }
162
163     Map<String, OxmEntityDescriptor> entityDescriptors = oxmEntityLookup.getEntityDescriptors();
164
165     try {
166
167       URIBuilder urlBuilder = new URIBuilder(link);
168       String urlPath = urlBuilder.getPath();
169
170       OxmEntityDescriptor descriptor = null;
171       String[] urlPathElements = urlPath.split("/");
172       List<String> primaryKeyNames = null;
173       int index = 0;
174       String entityType = null;
175
176       while (index < urlPathElements.length) {
177
178         descriptor = entityDescriptors.get(urlPathElements[index]);
179
180         if (descriptor != null) {
181           entityType = urlPathElements[index];
182           primaryKeyNames = descriptor.getPrimaryKeyAttributeNames();
183
184           /*
185            * Make sure from what ever index we matched the parent entity-type on that we can extract
186            * additional path elements for the primary key values.
187            */
188
189           if (index + primaryKeyNames.size() < urlPathElements.length) {
190
191             for (String primaryKeyName : primaryKeyNames) {
192               index++;
193               queryParams.add(entityType + "." + primaryKeyName + ":" + urlPathElements[index]);
194             }
195           } else {
196             LOG.error(AaiUiMsgs.QUERY_PARAM_EXTRACTION_ERROR,
197                 "Could not extract query parametrs for entity-type = '" + entityType
198                     + "' from self-link = " + link);
199           }
200         }
201
202         index++;
203       }
204
205     } catch (URISyntaxException exc) {
206
207       LOG.error(AaiUiMsgs.QUERY_PARAM_EXTRACTION_ERROR,
208           "Error extracting query parameters from self-link = " + link + ". Error = "
209               + exc.getMessage());
210     }
211
212     return queryParams;
213
214   }
215   
216   /**
217    * Decode complex attribute group.
218    *
219    * @param ain the ain
220    * @param attributeGroup the attribute group
221    * @return boolean indicating whether operation was successful (true), / failure(false).
222    */
223   public boolean decodeComplexAttributeGroup(ActiveInventoryNode ain, JsonNode attributeGroup) {
224
225     try {
226
227       Iterator<Entry<String, JsonNode>> entityArrays = attributeGroup.fields();
228       Entry<String, JsonNode> entityArray = null;
229
230       if (entityArrays == null) {
231         LOG.error(AaiUiMsgs.ATTRIBUTE_GROUP_FAILURE, attributeGroup.toString());
232         ain.changeState(NodeProcessingState.ERROR, NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
233         return false;
234       }
235
236       while (entityArrays.hasNext()) {
237
238         entityArray = entityArrays.next();
239
240         String entityType = entityArray.getKey();
241         JsonNode entityArrayObject = entityArray.getValue();
242
243         if (entityArrayObject.isArray()) {
244
245           Iterator<JsonNode> entityCollection = entityArrayObject.elements();
246           JsonNode entity = null;
247           while (entityCollection.hasNext()) {
248             entity = entityCollection.next();
249
250             if (LOG.isDebugEnabled()) {
251               LOG.debug(AaiUiMsgs.DEBUG_GENERIC, "decodeComplexAttributeGroup(),"
252                   + " entity = " + entity.toString());
253             }
254
255             /**
256              * Here's what we are going to do:
257              * 
258              * <li>In the ActiveInventoryNode, on construction maintain a collection of queryParams
259              * that is added to for the purpose of discovering parent->child hierarchies.
260              * 
261              * <li>When we hit this block of the code then we'll use the queryParams to feed the
262              * generic query to resolve the self-link asynchronously.
263              * 
264              * <li>Upon successful link determination, then and only then will we create a new node
265              * in the nodeCache and process the child
266              * 
267              */
268
269             ActiveInventoryNode newNode = new ActiveInventoryNode(this.visualizationConfigs, oxmEntityLookup);
270             newNode.setEntityType(entityType);
271
272             /*
273              * This is partially a lie because we actually don't have a self-link for complex nodes
274              * discovered in this way.
275              */
276             newNode.setSelfLinkProcessed(true);
277             newNode.changeState(NodeProcessingState.SELF_LINK_RESPONSE_UNPROCESSED,
278                 NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_OK);
279             
280             /*
281              * copy parent query params into new child
282              */
283             
284             if (SERVICE_INSTANCE.equals(entityType)) {
285               
286               /*
287                * 1707 AAI has an issue being tracked with AAI-8932 where the generic-query cannot be
288                * resolved if all the service-instance path keys are provided. The query only works
289                * if only the service-instance key and valude are passed due to a historical reason.
290                * A fix is being worked on for 1707, and when it becomes available we can revert this
291                * small change.
292                */
293               
294               newNode.clearQueryParams();
295               
296             } else {
297
298               /*
299                * For all other entity-types we want to copy the parent query parameters into the new node
300                * query parameters.
301                */
302
303               for (String queryParam : ain.getQueryParams()) {
304                 newNode.addQueryParam(queryParam);
305               }
306
307             }
308             
309             
310             if (!addComplexGroupToNode(newNode, entity)) {
311               LOG.error(AaiUiMsgs.ATTRIBUTE_GROUP_FAILURE, "Failed to add child to parent for child = " +  entity.toString());
312             }
313
314             if (!addNodeQueryParams(newNode)) {
315               LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE_NODE_ID, "Error determining node id and key for node = " + newNode.dumpNodeTree(true)
316                   + " skipping relationship processing");
317               newNode.changeState(NodeProcessingState.ERROR,
318                   NodeProcessingAction.NODE_IDENTITY_ERROR);
319               return false;
320             } else {
321
322               newNode.changeState(NodeProcessingState.NEIGHBORS_UNPROCESSED,
323                   NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_OK);
324
325             }
326             
327
328             /*
329              * Order matters for the query params. We need to set the parent ones before the child
330              * node
331              */
332
333             String selfLinkQuery =
334                 aaiAdapter.getGenericQueryForSelfLink(entityType, newNode.getQueryParams());
335
336             /**
337              * <li>get the self-link
338              * <li>add it to the new node
339              * <li>generate node id
340              * <li>add node to node cache
341              * <li>add node id to parent outbound links list
342              * <li>process node children (should be automatic) (but don't query and resolve
343              * self-link as we already have all the data)
344              */
345
346             SelfLinkDeterminationTransaction txn = new SelfLinkDeterminationTransaction();
347
348             txn.setQueryString(selfLinkQuery);
349             txn.setNewNode(newNode);
350             txn.setParentNodeId(ain.getNodeId());
351             aaiWorkOnHand.incrementAndGet();
352             supplyAsync(new PerformSelfLinkDeterminationTask(txn, null, aaiAdapter),
353                 aaiExecutorService).whenComplete((nodeTxn, error) -> {
354                   aaiWorkOnHand.decrementAndGet();
355                   if (error != null) {
356                     LOG.error(AaiUiMsgs.SELF_LINK_DETERMINATION_FAILED_GENERIC, selfLinkQuery);
357                   } else {
358
359                     OperationResult opResult = nodeTxn.getOpResult();
360
361                     ActiveInventoryNode newChildNode = txn.getNewNode();
362
363                     if (opResult != null && opResult.wasSuccessful()) {
364
365                       if (!opResult.wasSuccessful()) {
366                         numFailedLinkResolve.incrementAndGet();
367                       }
368
369                       if (opResult.isFromCache()) {
370                         numSuccessfulLinkResolveFromCache.incrementAndGet();
371                       } else {
372                         numSuccessfulLinkResolveFromFromServer.incrementAndGet();
373                       }
374
375                       /*
376                        * extract the self-link from the operational result.
377                        */
378
379                       Collection<JsonNode> entityLinks = new ArrayList<JsonNode>();
380                       JsonNode genericQueryResult = null;
381                       try {
382                         genericQueryResult =
383                             NodeUtils.convertJsonStrToJsonNode(nodeTxn.getOpResult().getResult());
384                       } catch (Exception exc) {
385                         LOG.error(AaiUiMsgs.JSON_CONVERSION_ERROR, JsonNode.class.toString(), exc.getMessage());
386                       }
387
388                       NodeUtils.extractObjectsByKey(genericQueryResult, "resource-link",
389                           entityLinks);
390
391                       String selfLink = null;
392
393                       if (entityLinks.size() != 1) {
394
395                         LOG.error(AaiUiMsgs.SELF_LINK_DETERMINATION_FAILED_UNEXPECTED_LINKS, String.valueOf(entityLinks.size()));
396                           
397                       } else {
398                         selfLink = ((JsonNode) entityLinks.toArray()[0]).asText();
399                         selfLink = ActiveInventoryAdapter.extractResourcePath(selfLink);
400
401                         newChildNode.setSelfLink(selfLink);
402                         newChildNode.setNodeId(NodeUtils.generateUniqueShaDigest(selfLink));
403
404                         String uri = NodeUtils.calculateEditAttributeUri(selfLink);
405                         if (uri != null) {
406                           newChildNode.addProperty(SparkyConstants.URI_ATTR_NAME, uri);
407                         }
408                         
409                         ActiveInventoryNode parent = nodeCache.get(txn.getParentNodeId());
410
411                         if (parent != null) {
412                           parent.addOutboundNeighbor(newChildNode.getNodeId());
413                           newChildNode.addInboundNeighbor(parent.getNodeId());
414                         }
415
416                         newChildNode.setSelfLinkPendingResolve(false);
417                         newChildNode.setSelfLinkProcessed(true);
418                         newChildNode.changeState(NodeProcessingState.NEIGHBORS_UNPROCESSED,
419                               NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_OK);
420                         
421                         nodeCache.putIfAbsent(newChildNode.getNodeId(), newChildNode);
422                         
423                       }
424
425                     } else {
426                       LOG.error(AaiUiMsgs.SELF_LINK_RETRIEVAL_FAILED, txn.getQueryString(),
427                           String.valueOf(nodeTxn.getOpResult().getResultCode()), nodeTxn.getOpResult().getResult());
428                       newChildNode.setSelflinkRetrievalFailure(true);
429                       newChildNode.setSelfLinkProcessed(true);
430                       newChildNode.setSelfLinkPendingResolve(false);
431
432                       newChildNode.changeState(NodeProcessingState.ERROR,
433                           NodeProcessingAction.SELF_LINK_DETERMINATION_ERROR);
434
435                     }
436
437                   }
438
439                 });
440
441           }
442
443           return true;
444
445         } else {
446           LOG.error(AaiUiMsgs.UNHANDLED_OBJ_TYPE_FOR_ENTITY_TYPE, entityType);
447         }
448
449       }
450     } catch (Exception exc) {
451       LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Exception caught while"
452           + " decoding complex attribute group - " + exc.getMessage());
453     }
454
455     return false;
456
457   }
458
459   /**
460    * Process self link response.
461    *
462    * @param nodeId the node id
463    */
464   private void processSelfLinkResponse(String nodeId) {
465
466     if (nodeId == null) {
467       LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Cannot process self link"
468           + " response because nodeId is null");
469       return;
470     }
471
472     ActiveInventoryNode ain = nodeCache.get(nodeId);
473
474     if (ain == null) {
475       LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Cannot process self link response"
476           + " because can't find node for id = " + nodeId);
477       return;
478     }
479
480     JsonNode jsonNode = null;
481
482     try {
483       jsonNode = mapper.readValue(ain.getOpResult().getResult(), JsonNode.class);
484     } catch (Exception exc) {
485       LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to marshal json"
486           + " response str into JsonNode with error, " + exc.getLocalizedMessage());
487       ain.changeState(NodeProcessingState.ERROR,
488           NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_ERROR);
489       return;
490     }
491
492     if (jsonNode == null) {
493       LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to parse json node str."
494           + " Parse resulted a null value.");
495       ain.changeState(NodeProcessingState.ERROR,
496           NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_ERROR);
497       return;
498     }
499
500     Iterator<Entry<String, JsonNode>> fieldNames = jsonNode.fields();
501     Entry<String, JsonNode> field = null;
502
503     RelationshipList relationshipList = null;
504
505     while (fieldNames.hasNext()) {
506
507       field = fieldNames.next();
508       String fieldName = field.getKey();
509
510       if ("relationship-list".equals(fieldName)) {
511
512         try {
513           relationshipList = mapper.readValue(field.getValue().toString(), RelationshipList.class);
514
515           if (relationshipList != null) {
516             ain.addRelationshipList(relationshipList);
517           }
518
519         } catch (Exception exc) {
520           LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to parse relationship-list"
521               + " attribute. Parse resulted in error, " + exc.getLocalizedMessage());
522           ain.changeState(NodeProcessingState.ERROR,
523               NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_ERROR);
524           return;
525         }
526
527       } else {
528
529         JsonNode nodeValue = field.getValue();
530
531         if (nodeValue != null && nodeValue.isValueNode()) {
532
533           if (oxmEntityLookup.getEntityDescriptors().get(fieldName) == null) {
534
535             /*
536              * entity property name is not an entity, thus we can add this property name and value
537              * to our property set
538              */
539
540             ain.addProperty(fieldName, nodeValue.asText());
541
542           }
543
544         } else {
545
546           if (nodeValue.isArray()) {
547
548             if (oxmEntityLookup.getEntityDescriptors().get(fieldName) == null) {
549
550               /*
551                * entity property name is not an entity, thus we can add this property name and value
552                * to our property set
553                */
554
555               ain.addProperty(field.getKey(), nodeValue.toString());
556
557             }
558
559           } else {
560
561             ain.addComplexGroup(nodeValue);
562
563           }
564
565         }
566       }
567
568     }
569
570     String uri = NodeUtils.calculateEditAttributeUri(ain.getSelfLink());
571     if (uri != null) {
572       ain.addProperty(SparkyConstants.URI_ATTR_NAME, uri);
573     }
574
575     /*
576      * We need a special behavior for intermediate entities from the REST model
577      * 
578      * Tenants are not top level entities, and when we want to visualization
579      * their children, we need to construct keys that include the parent entity query
580      * keys, the current entity type keys, and the child keys.   We'll always have the
581      * current entity and children, but never the parent entity in the current (1707) REST
582      * data model.
583      * 
584      * We have two possible solutions:
585      * 
586      * 1) Try to use the custom-query approach to learn about the entity keys
587      *    - this could be done, but it could be very expensive for large objects.  When we do the first
588      *      query to get a tenant, it will list all the in and out edges related to this entity,
589      *      there is presently no way to filter this.  But the approach could be made to work and it would be
590      *      somewhat data-model driven, other than the fact that we have to first realize that the entity
591      *      that is being searched for is not top-level entity.  Once we have globally unique ids for resources
592      *      this logic will not be needed and everything will be simpler.   The only reason we are in this logic
593      *      at all is to be able to calculate a url for the child entities so we can hash it to generate 
594      *      a globally unique id that can be safely used for the node.
595      *      
596      * *2* Extract the keys from the pathed self-link.
597      *     This is a bad solution and I don't like it but it will be fast for all resource types, as the 
598      *     information is already encoded in the URI.   When we get to a point where we switch to a better
599      *     globally unique entity identity model, then a lot of the code being used to calculate an entity url
600      *     to in-turn generate a deterministic globally unique id will disappear.      
601      *     
602      * 
603      * right now we have the following:
604      * 
605      * - cloud-regions/cloud-region/{cloud-region-id}/{cloud-owner-id}/tenants/tenant/{tenant-id}
606      *  
607      */
608
609     /*
610      * For all entity types use the self-link extraction method to be consistent.  Once we have a
611      * globally unique identity mechanism for entities, this logic can be revisited.
612      */
613     ain.clearQueryParams();
614     ain.addQueryParams(extractQueryParamsFromSelfLink(ain.getSelfLink()));
615       ain.changeState(NodeProcessingState.NEIGHBORS_UNPROCESSED,
616           NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_OK);
617     
618
619   }
620
621   /**
622    * Perform self link resolve.
623    *
624    * @param nodeId the node id
625    */
626   private void performSelfLinkResolve(String nodeId) {
627
628     if (nodeId == null) {
629       LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Resolve of self-link"
630           + " has been skipped because provided nodeId is null");
631       return;
632     }
633
634     ActiveInventoryNode ain = nodeCache.get(nodeId);
635
636     if (ain == null) {
637       LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Failed to find node with id, " + nodeId
638           + ", from node cache. Resolve self-link method has been skipped.");
639       return;
640     }
641
642     if (!ain.isSelfLinkPendingResolve()) {
643
644       ain.setSelfLinkPendingResolve(true);
645
646       // kick off async self-link resolution
647
648       if (LOG.isDebugEnabled()) {
649         LOG.debug(AaiUiMsgs.DEBUG_GENERIC, 
650             "About to process node in SELF_LINK_UNPROCESSED State, link = " + ain.getSelfLink());
651       }
652
653       numLinksDiscovered.incrementAndGet();
654
655       String depthModifier = DEPTH_ALL_MODIFIER;
656
657       /*
658        * If the current node is the search target, we want to see everything the node has to offer
659        * from the self-link and not filter it to a single node.
660        */
661
662       if (visualizationConfigs.getShallowEntities().contains(ain.getEntityType())
663           && !ain.isRootNode()) {
664         depthModifier = NODES_ONLY_MODIFIER;
665       }
666
667       NodeProcessingTransaction txn = new NodeProcessingTransaction();
668       txn.setProcessingNode(ain);
669       txn.setRequestParameters(depthModifier);
670       aaiWorkOnHand.incrementAndGet();
671       supplyAsync(
672           new PerformNodeSelfLinkProcessingTask(txn, depthModifier, aaiAdapter),
673           aaiExecutorService).whenComplete((nodeTxn, error) -> {
674             aaiWorkOnHand.decrementAndGet();
675             if (error != null) {
676
677               /*
678                * an error processing the self link should probably result in the node processing
679                * state shifting to ERROR
680                */
681
682               nodeTxn.getProcessingNode().setSelflinkRetrievalFailure(true);
683
684               nodeTxn.getProcessingNode().changeState(NodeProcessingState.ERROR,
685                   NodeProcessingAction.SELF_LINK_RESOLVE_ERROR);
686
687               nodeTxn.getProcessingNode().setSelfLinkPendingResolve(false);
688
689             } else {
690
691               totalLinksRetrieved.incrementAndGet();
692
693               OperationResult opResult = nodeTxn.getOpResult();
694
695               if (opResult != null && opResult.wasSuccessful()) {
696
697                 if (!opResult.wasSuccessful()) {
698                   numFailedLinkResolve.incrementAndGet();
699                 }
700
701                 if (opResult.isFromCache()) {
702                   numSuccessfulLinkResolveFromCache.incrementAndGet();
703                 } else {
704                   numSuccessfulLinkResolveFromFromServer.incrementAndGet();
705                 }
706
707                 // success path
708                 nodeTxn.getProcessingNode().setOpResult(opResult);
709                 nodeTxn.getProcessingNode().changeState(
710                     NodeProcessingState.SELF_LINK_RESPONSE_UNPROCESSED,
711                     NodeProcessingAction.SELF_LINK_RESOLVE_OK);
712
713                 nodeTxn.getProcessingNode().setSelfLinkProcessed(true);
714                 nodeTxn.getProcessingNode().setSelfLinkPendingResolve(false);
715
716               } else {
717                 LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Self Link retrieval for link,"
718                     + txn.getSelfLinkWithModifiers() + ", failed with error code,"
719                     + nodeTxn.getOpResult().getResultCode() + ", and message,"
720                     + nodeTxn.getOpResult().getResult());
721
722                 nodeTxn.getProcessingNode().setSelflinkRetrievalFailure(true);
723                 nodeTxn.getProcessingNode().setSelfLinkProcessed(true);
724
725                 nodeTxn.getProcessingNode().changeState(NodeProcessingState.ERROR,
726                     NodeProcessingAction.SELF_LINK_RESOLVE_ERROR);
727
728                 nodeTxn.getProcessingNode().setSelfLinkPendingResolve(false);
729
730               }
731             }
732
733           });
734
735     }
736
737   }
738
739
740   /**
741    * Process neighbors.
742    *
743    * @param nodeId the node id
744    */
745   private void processNeighbors(String nodeId) {
746     
747     if (nodeId == null) {
748       LOG.error(AaiUiMsgs.SELF_LINK_PROCESS_NEIGHBORS_ERROR, "Failed to process"
749           + " neighbors because nodeId is null.");
750       return;
751     }
752
753     ActiveInventoryNode ain = nodeCache.get(nodeId);
754
755     if (ain == null) {
756       LOG.error(AaiUiMsgs.SELF_LINK_PROCESS_NEIGHBORS_ERROR, "Failed to process"
757           + " neighbors because node could not be found in nodeCache with id, " + nodeId);
758       return;
759     }
760
761     /*
762      * process complex attribute and relationships
763      */
764
765     boolean neighborsProcessedSuccessfully = true;
766
767     for (JsonNode n : ain.getComplexGroups()) {
768       neighborsProcessedSuccessfully &= decodeComplexAttributeGroup(ain, n);
769     }
770
771     for (RelationshipList relationshipList : ain.getRelationshipLists()) {
772       neighborsProcessedSuccessfully &= addSelfLinkRelationshipChildren(ain, relationshipList);
773     }
774
775
776     if (neighborsProcessedSuccessfully) {
777       ain.changeState(NodeProcessingState.READY, NodeProcessingAction.NEIGHBORS_PROCESSED_OK);
778     } else {
779       ain.changeState(NodeProcessingState.ERROR, NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
780     }
781   
782
783     /*
784      * If neighbors fail to process, there is already a call to change the state within the
785      * relationship and neighbor processing functions.
786      */
787
788   }
789
790   /**
791    * Find and mark root node.
792    *
793    * @param queryParams the query params
794    * @return true, if successful
795    */
796   private boolean findAndMarkRootNode(QueryParams queryParams) {
797
798     for (ActiveInventoryNode cacheNode : nodeCache.values()) {
799
800       if (queryParams.getSearchTargetNodeId().equals(cacheNode.getNodeId())) {
801         cacheNode.setNodeDepth(0);
802         cacheNode.setRootNode(true);
803         LOG.info(AaiUiMsgs.ROOT_NODE_DISCOVERED, queryParams.getSearchTargetNodeId());
804         return true;
805       }
806     }
807
808     return false;
809
810   }
811
812   /**
813    * Process current node states.
814    *
815    * @param rootNodeDiscovered the root node discovered
816    */
817   private void processCurrentNodeStates(boolean rootNodeDiscovered) {
818     /*
819      * Force an evaluation of node depths before determining if we should limit state-based
820      * traversal or processing.
821      */
822     if (rootNodeDiscovered) {
823       evaluateNodeDepths();
824     }
825
826     for (ActiveInventoryNode cacheNode : nodeCache.values()) {
827
828       if (LOG.isDebugEnabled()) {
829         LOG.debug(AaiUiMsgs.DEBUG_GENERIC, 
830             "processCurrentNodeState(), nid = "
831             + cacheNode.getNodeId() + " , nodeDepth = " + cacheNode.getNodeDepth());
832       }
833
834       switch (cacheNode.getState()) {
835
836         case INIT: {
837           processInitialState(cacheNode.getNodeId());
838           break;
839         }
840
841         case READY:
842         case ERROR: {
843           break;
844         }
845
846         case SELF_LINK_UNRESOLVED: {
847           performSelfLinkResolve(cacheNode.getNodeId());
848           break;
849         }
850
851         case SELF_LINK_RESPONSE_UNPROCESSED: {
852           processSelfLinkResponse(cacheNode.getNodeId());
853           break;
854         }
855
856         case NEIGHBORS_UNPROCESSED: {
857
858           /*
859            * We use the rootNodeDiscovered flag to ignore depth retrieval thresholds until the root
860            * node is identified. Then the evaluative depth calculations should re-balance the graph
861            * around the root node.
862            */
863           
864           if (!rootNodeDiscovered || cacheNode.getNodeDepth() < this.visualizationConfigs.getMaxSelfLinkTraversalDepth()) {
865
866             if (LOG.isDebugEnabled()) {
867               LOG.debug(AaiUiMsgs.DEBUG_GENERIC, 
868                   "SLNC::processCurrentNodeState() -- Node at max depth,"
869                   + " halting processing at current state = -- "
870                       + cacheNode.getState() + " nodeId = " + cacheNode.getNodeId());
871             }
872
873             
874             
875             processNeighbors(cacheNode.getNodeId());
876
877           }
878
879           break;
880         }
881         default:
882           break;
883
884
885
886       }
887
888     }
889
890   }
891
892   /**
893    * Adds the complex group to node.
894    *
895    * @param targetNode the target node
896    * @param attributeGroup the attribute group
897    * @return true, if successful
898    */
899   private boolean addComplexGroupToNode(ActiveInventoryNode targetNode, JsonNode attributeGroup) {
900
901     if (attributeGroup == null) {
902       targetNode.changeState(NodeProcessingState.ERROR,
903           NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_OK);
904       return false;
905     }
906
907     RelationshipList relationshipList = null;
908
909     if (attributeGroup.isObject()) {
910
911       Iterator<Entry<String, JsonNode>> fields = attributeGroup.fields();
912       Entry<String, JsonNode> field = null;
913       String fieldName;
914       JsonNode fieldValue;
915
916       while (fields.hasNext()) {
917         field = fields.next();
918         fieldName = field.getKey();
919         fieldValue = field.getValue();
920
921         if (fieldValue.isObject()) {
922
923           if (fieldName.equals("relationship-list")) {
924
925             try {
926               relationshipList =
927                   mapper.readValue(field.getValue().toString(), RelationshipList.class);
928
929               if (relationshipList != null) {
930                 targetNode.addRelationshipList(relationshipList);
931               }
932
933             } catch (Exception exc) {
934               LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to parse"
935                   + " relationship-list attribute. Parse resulted in error, "
936                   + exc.getLocalizedMessage());
937               targetNode.changeState(NodeProcessingState.ERROR,
938                   NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_ERROR);
939               return false;
940             }
941
942           } else {
943             targetNode.addComplexGroup(fieldValue);
944           }
945
946         } else if (fieldValue.isArray()) {
947           if (LOG.isDebugEnabled()) {
948             LOG.debug(AaiUiMsgs.DEBUG_GENERIC, 
949                 "Unexpected array type with a key = " + fieldName);
950           }
951         } else if (fieldValue.isValueNode()) {
952           if (oxmEntityLookup.getEntityDescriptors().get(field.getKey()) == null) {
953             /*
954              * property key is not an entity type, add it to our property set.
955              */
956             targetNode.addProperty(field.getKey(), fieldValue.asText());
957           }
958
959         }
960       }
961
962     } else if (attributeGroup.isArray()) {
963       if (LOG.isDebugEnabled()) {
964         LOG.debug(AaiUiMsgs.DEBUG_GENERIC, 
965             "Unexpected array type for attributeGroup = " + attributeGroup);
966       }
967     } else if (attributeGroup.isValueNode()) {
968       if (LOG.isDebugEnabled()) {
969         LOG.debug(AaiUiMsgs.DEBUG_GENERIC, 
970             "Unexpected value type for attributeGroup = " + attributeGroup);
971       }
972     }
973     
974     return true;
975   }
976   
977   public int getNumSuccessfulLinkResolveFromCache() {
978     return numSuccessfulLinkResolveFromCache.get();
979   }
980
981   public int getNumSuccessfulLinkResolveFromFromServer() {
982     return numSuccessfulLinkResolveFromFromServer.get();
983   }
984
985   public int getNumFailedLinkResolve() {
986     return numFailedLinkResolve.get();
987   }
988
989   public InlineMessage getInlineMessage() {
990     return inlineMessage;
991   }
992
993   public void setInlineMessage(InlineMessage inlineMessage) {
994     this.inlineMessage = inlineMessage;
995   }
996
997   public void setMaxSelfLinkTraversalDepth(int depth) {
998     this.maxSelfLinkTraversalDepth = depth;
999   }
1000
1001   public int getMaxSelfLinkTraversalDepth() {
1002     return this.maxSelfLinkTraversalDepth;
1003   }
1004
1005   public ConcurrentHashMap<String, ActiveInventoryNode> getNodeCache() {
1006     return nodeCache;
1007   }
1008
1009   /**
1010    * Gets the relationship primary key values.
1011    *
1012    * @param r the r
1013    * @param entityType the entity type
1014    * @param pkeyNames the pkey names
1015    * @return the relationship primary key values
1016    */
1017   private String getRelationshipPrimaryKeyValues(Relationship r, String entityType,
1018       List<String> pkeyNames) {
1019
1020     StringBuilder sb = new StringBuilder(64);
1021
1022     if (pkeyNames.size() > 0) {
1023       String primaryKey = extractKeyValueFromRelationData(r, entityType + "." + pkeyNames.get(0));
1024       if (primaryKey != null) {
1025
1026         sb.append(primaryKey);
1027
1028       } else {
1029         // this should be a fatal error because unless we can
1030         // successfully retrieve all the expected keys we'll end up
1031         // with a garbage node
1032         LOG.error(AaiUiMsgs.EXTRACTION_ERROR, "ERROR: Failed to extract"
1033             + " keyName, " + entityType + "." + pkeyNames.get(0)
1034             + ", from relationship data, " + r.toString());
1035         return null;
1036       }
1037
1038       for (int i = 1; i < pkeyNames.size(); i++) {
1039
1040         String kv = extractKeyValueFromRelationData(r, entityType + "." + pkeyNames.get(i));
1041         if (kv != null) {
1042           sb.append("/").append(kv);
1043         } else {
1044           // this should be a fatal error because unless we can
1045           // successfully retrieve all the expected keys we'll end up
1046           // with a garbage node
1047           LOG.error(AaiUiMsgs.EXTRACTION_ERROR, "ERROR:  failed to extract keyName, "
1048               + entityType + "." + pkeyNames.get(i)
1049               + ", from relationship data, " + r.toString());
1050           return null;
1051         }
1052       }
1053
1054       return sb.toString();
1055
1056     }
1057
1058     return null;
1059
1060   }
1061
1062   /**
1063    * Extract key value from relation data.
1064    *
1065    * @param r the r
1066    * @param keyName the key name
1067    * @return the string
1068    */
1069   private String extractKeyValueFromRelationData(Relationship r, String keyName) {
1070
1071     RelationshipData[] rdList = r.getRelationshipData();
1072
1073     for (RelationshipData relData : rdList) {
1074
1075       if (relData.getRelationshipKey().equals(keyName)) {
1076         return relData.getRelationshipValue();
1077       }
1078     }
1079
1080     return null;
1081   }
1082
1083   /**
1084    * Determine node id and key.
1085    *
1086    * @param ain the ain
1087    * @return true, if successful
1088    */
1089   private boolean addNodeQueryParams(ActiveInventoryNode ain) {
1090
1091     if (ain == null) {
1092       LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE_NODE_ID, "ActiveInventoryNode is null");
1093       return false;
1094     }
1095
1096     List<String> pkeyNames =
1097         oxmEntityLookup.getEntityDescriptors().get(ain.getEntityType()).getPrimaryKeyAttributeNames();
1098
1099     if (pkeyNames == null || pkeyNames.size() == 0) {
1100       LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE_NODE_ID, "Primary key names is empty");
1101       return false;
1102     }
1103
1104     StringBuilder sb = new StringBuilder(64);
1105
1106     if (pkeyNames.size() > 0) {
1107       String primaryKey = ain.getProperties().get(pkeyNames.get(0));
1108       if (primaryKey != null) {
1109         sb.append(primaryKey);
1110       } else {
1111         // this should be a fatal error because unless we can
1112         // successfully retrieve all the expected keys we'll end up
1113         // with a garbage node
1114         LOG.error(AaiUiMsgs.EXTRACTION_ERROR, "ERROR: Failed to extract keyName, "
1115             + pkeyNames.get(0) + ", from entity properties");
1116         return false;
1117       }
1118
1119       for (int i = 1; i < pkeyNames.size(); i++) {
1120
1121         String kv = ain.getProperties().get(pkeyNames.get(i));
1122         if (kv != null) {
1123           sb.append("/").append(kv);
1124         } else {
1125           // this should be a fatal error because unless we can
1126           // successfully retrieve all the expected keys we'll end up
1127           // with a garbage node
1128           LOG.error(AaiUiMsgs.EXTRACTION_ERROR, "ERROR: Failed to extract keyName, "
1129               + pkeyNames.get(i) + ", from entity properties");
1130           return false;
1131         }
1132       }
1133
1134       /*final String nodeId = NodeUtils.generateUniqueShaDigest(ain.getEntityType(),
1135           NodeUtils.concatArray(pkeyNames, "/"), sb.toString());*/
1136
1137       //ain.setNodeId(nodeId);
1138       ain.setPrimaryKeyName(NodeUtils.concatArray(pkeyNames, "/"));
1139       ain.setPrimaryKeyValue(sb.toString());
1140       
1141       if (ain.getEntityType() != null && ain.getPrimaryKeyName() != null
1142           && ain.getPrimaryKeyValue() != null) {
1143         ain.addQueryParam(
1144             ain.getEntityType() + "." + ain.getPrimaryKeyName() + ":" + ain.getPrimaryKeyValue());
1145       }
1146       return true;
1147
1148     }
1149
1150     return false;
1151
1152   }
1153
1154   /**
1155    * Adds the self link relationship children.
1156    *
1157    * @param processingNode the processing node
1158    * @param relationshipList the relationship list
1159    * @return true, if successful
1160    */
1161   private boolean addSelfLinkRelationshipChildren(ActiveInventoryNode processingNode,
1162       RelationshipList relationshipList) {
1163
1164     if (relationshipList == null) {
1165       LOG.debug(AaiUiMsgs.DEBUG_GENERIC, "No relationships added to parent node = "
1166           + processingNode.getNodeId() + " because relationshipList is empty");
1167       processingNode.changeState(NodeProcessingState.ERROR,
1168           NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
1169       return false;
1170     }
1171
1172     Relationship[] relationshipArray = relationshipList.getRelationshipList();
1173     OxmEntityDescriptor descriptor = null;
1174     String repairedSelfLink = null;
1175
1176     if (relationshipArray != null) {
1177
1178       ActiveInventoryNode newNode = null;
1179       String resourcePath = null;
1180
1181       for (Relationship r : relationshipArray) {
1182         
1183         resourcePath = ActiveInventoryAdapter.extractResourcePath(r.getRelatedLink());
1184
1185         String nodeId = NodeUtils.generateUniqueShaDigest(resourcePath);
1186
1187         if (nodeId == null) {
1188
1189           LOG.error(AaiUiMsgs.SKIPPING_RELATIONSHIP, r.toString());
1190           processingNode.changeState(NodeProcessingState.ERROR,
1191               NodeProcessingAction.NODE_IDENTITY_ERROR);
1192           return false;
1193         }
1194
1195         newNode = new ActiveInventoryNode(this.visualizationConfigs, oxmEntityLookup);
1196
1197         String entityType = r.getRelatedTo();
1198
1199         if (r.getRelationshipData() != null) {
1200           for (RelationshipData rd : r.getRelationshipData()) {
1201             newNode.addQueryParam(rd.getRelationshipKey() + ":" + rd.getRelationshipValue());
1202           }
1203         }
1204
1205         descriptor = oxmEntityLookup.getEntityDescriptors().get(r.getRelatedTo());
1206
1207         newNode.setNodeId(nodeId);
1208         newNode.setEntityType(entityType);
1209         newNode.setSelfLink(resourcePath);
1210
1211         processingNode.addOutboundNeighbor(nodeId);
1212
1213         if (descriptor != null) {
1214
1215           List<String> pkeyNames = descriptor.getPrimaryKeyAttributeNames();
1216
1217           newNode.changeState(NodeProcessingState.SELF_LINK_UNRESOLVED,
1218               NodeProcessingAction.SELF_LINK_SET);
1219
1220           newNode.setPrimaryKeyName(NodeUtils.concatArray(pkeyNames, "/"));
1221
1222           String primaryKeyValues = getRelationshipPrimaryKeyValues(r, entityType, pkeyNames);
1223           newNode.setPrimaryKeyValue(primaryKeyValues);
1224
1225         } else {
1226
1227           LOG.error(AaiUiMsgs.VISUALIZATION_OUTPUT_ERROR,
1228               "Failed to parse entity because OXM descriptor could not be found for type = "
1229                   + r.getRelatedTo());
1230
1231           newNode.changeState(NodeProcessingState.ERROR,
1232               NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
1233
1234         }
1235
1236         if (nodeCache.putIfAbsent(nodeId, newNode) != null) {
1237           if (LOG.isDebugEnabled()) {
1238             LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
1239                 "Failed to add node to nodeCache because it already exists.  Node id = "
1240                     + newNode.getNodeId());
1241           }
1242         }
1243
1244       }
1245
1246     }
1247
1248     return true;
1249
1250   }
1251
1252   /**
1253    * Process initial state.
1254    *
1255    * @param nodeId the node id
1256    */
1257   private void processInitialState(String nodeId) {
1258
1259     if (nodeId == null) {
1260       LOG.error(AaiUiMsgs.FAILED_TO_PROCESS_INITIAL_STATE, "Node id is null");
1261       return;
1262     }
1263
1264     ActiveInventoryNode cachedNode = nodeCache.get(nodeId);
1265
1266     if (cachedNode == null) {
1267       LOG.error(AaiUiMsgs.FAILED_TO_PROCESS_INITIAL_STATE, "Node cannot be"
1268           + " found for nodeId, " + nodeId);
1269       return;
1270     }
1271
1272     if (cachedNode.getSelfLink() == null) {
1273
1274       if (cachedNode.getNodeId() == null ) {
1275
1276         /*
1277          * if the self link is null at the INIT state, which could be valid if this node is a
1278          * complex attribute group which didn't originate from a self-link, but in that situation
1279          * both the node id and node key should already be set.
1280          */
1281
1282         cachedNode.changeState(NodeProcessingState.ERROR, NodeProcessingAction.NODE_IDENTITY_ERROR);
1283
1284       }
1285
1286       if (cachedNode.getNodeId() != null) {
1287
1288         /*
1289          * This should be the success path branch if the self-link is not set
1290          */
1291
1292         cachedNode.changeState(NodeProcessingState.SELF_LINK_RESPONSE_UNPROCESSED,
1293             NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_OK);
1294
1295       }
1296
1297     } else {
1298
1299       if (cachedNode.hasResolvedSelfLink()) {
1300         LOG.error(AaiUiMsgs.INVALID_RESOLVE_STATE_DURING_INIT);
1301         cachedNode.changeState(NodeProcessingState.ERROR,
1302             NodeProcessingAction.UNEXPECTED_STATE_TRANSITION);
1303       } else {
1304         cachedNode.changeState(NodeProcessingState.SELF_LINK_UNRESOLVED,
1305             NodeProcessingAction.SELF_LINK_SET);
1306       }
1307     }
1308   }
1309
1310   /**
1311    * Process skeleton node.
1312    *
1313    * @param skeletonNode the skeleton node
1314    * @param queryParams the query params
1315    */
1316   private void processSearchableEntity(SearchableEntity searchTargetEntity, QueryParams queryParams) {
1317
1318     if (searchTargetEntity == null) {
1319       return;
1320     }
1321
1322     if (searchTargetEntity.getId() == null) {
1323       LOG.error(AaiUiMsgs.FAILED_TO_PROCESS_SKELETON_NODE, "Failed to process skeleton"
1324           + " node because nodeId is null for node, " + searchTargetEntity.getLink());
1325       return;
1326     }
1327
1328     ActiveInventoryNode newNode = new ActiveInventoryNode(this.visualizationConfigs, oxmEntityLookup);
1329
1330     newNode.setNodeId(searchTargetEntity.getId());
1331     newNode.setEntityType(searchTargetEntity.getEntityType());
1332     newNode.setPrimaryKeyName(getEntityTypePrimaryKeyName(searchTargetEntity.getEntityType()));
1333     newNode.setPrimaryKeyValue(searchTargetEntity.getEntityPrimaryKeyValue());
1334     
1335     if (newNode.getEntityType() != null && newNode.getPrimaryKeyName() != null
1336         && newNode.getPrimaryKeyValue() != null) {
1337       newNode.addQueryParam(
1338           newNode.getEntityType() + "." + newNode.getPrimaryKeyName() + ":" + newNode.getPrimaryKeyValue());
1339     }
1340     /*
1341      * This code may need some explanation. In any graph there will be a single root node. The root
1342      * node is really the center of the universe, and for now, we are tagging the search target as
1343      * the root node. Everything else in the visualization of the graph will be centered around this
1344      * node as the focal point of interest.
1345      * 
1346      * Due to it's special nature, there will only ever be one root node, and it's node depth will
1347      * always be equal to zero.
1348      */
1349
1350     if (queryParams.getSearchTargetNodeId().equals(newNode.getNodeId())) {
1351       newNode.setNodeDepth(0);
1352       newNode.setRootNode(true);
1353       LOG.info(AaiUiMsgs.ROOT_NODE_DISCOVERED, queryParams.getSearchTargetNodeId());
1354     }
1355
1356     newNode.setSelfLink(searchTargetEntity.getLink());
1357
1358     nodeCache.putIfAbsent(newNode.getNodeId(), newNode);
1359   }
1360
1361   /**
1362    * Checks for out standing work.
1363    *
1364    * @return true, if successful
1365    */
1366   private boolean hasOutStandingWork() {
1367
1368     int numNodesWithPendingStates = 0;
1369
1370     /*
1371      * Force an evaluation of node depths before determining if we should limit state-based
1372      * traversal or processing.
1373      */
1374
1375     evaluateNodeDepths();
1376
1377     for (ActiveInventoryNode n : nodeCache.values()) {
1378
1379       switch (n.getState()) {
1380
1381         case READY:
1382         case ERROR: {
1383           // do nothing, these are our normal
1384           // exit states
1385           break;
1386         }
1387
1388         case NEIGHBORS_UNPROCESSED: {
1389
1390           if (n.getNodeDepth() < this.visualizationConfigs.getMaxSelfLinkTraversalDepth()) {
1391             /*
1392              * Only process our neighbors relationships if our current depth is less than the max
1393              * depth
1394              */
1395             numNodesWithPendingStates++;
1396           }
1397
1398           break;
1399         }
1400
1401         default: {
1402
1403           /*
1404            * for all other states, there is work to be done
1405            */
1406           numNodesWithPendingStates++;
1407         }
1408
1409       }
1410
1411     }
1412
1413     LOG.debug(AaiUiMsgs.OUTSTANDING_WORK_PENDING_NODES, String.valueOf(numNodesWithPendingStates));
1414
1415     return (numNodesWithPendingStates > 0);
1416
1417   }
1418
1419   /* (non-Javadoc)
1420    * @see org.onap.aai.sparky.viewandinspect.services.VisualizationContext#processSelfLinks(org.onap.aai.sparky.sync.entity.SearchableEntity, org.onap.aai.sparky.viewandinspect.entity.QueryParams)
1421    */
1422   @Override
1423   public void processSelfLinks(SearchableEntity searchtargetEntity, QueryParams queryParams) {
1424
1425     try {
1426
1427       if (searchtargetEntity == null) {
1428         LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, contextIdStr + " - Failed to"
1429             + " processSelfLinks, searchtargetEntity is null");
1430         return;
1431       }
1432
1433       processSearchableEntity(searchtargetEntity, queryParams);
1434
1435       long startTimeInMs = System.currentTimeMillis();
1436
1437       /*
1438        * wait until all transactions are complete or guard-timer expires.
1439        */
1440
1441       long totalResolveTime = 0;
1442       boolean hasOutstandingWork = hasOutStandingWork();
1443       boolean outstandingWorkGuardTimerFired = false;
1444       long maxGuardTimeInMs = 5000;
1445       long guardTimeInMs = 0;
1446       boolean foundRootNode = false;
1447
1448       
1449       /*
1450        * TODO:   Put a count-down-latch in place of the while loop, but if we do that then
1451        * we'll need to decouple the visualization processing from the main thread so it can continue to process while
1452        * the main thread is waiting on for count-down-latch gate to open.  This may also be easier once we move to the
1453        * VisualizationService + VisualizationContext ideas. 
1454        */
1455       
1456       
1457       while (hasOutstandingWork || !outstandingWorkGuardTimerFired) {
1458
1459         if (!foundRootNode) {
1460           foundRootNode = findAndMarkRootNode(queryParams);
1461         }
1462
1463         processCurrentNodeStates(foundRootNode);
1464
1465         verifyOutboundNeighbors();
1466
1467         try {
1468           Thread.sleep(500);
1469         } catch (InterruptedException exc) {
1470           LOG.error(AaiUiMsgs.PROCESSING_LOOP_INTERUPTED, exc.getMessage());
1471           return;
1472         }
1473
1474         totalResolveTime = (System.currentTimeMillis() - startTimeInMs);
1475
1476         if (!hasOutstandingWork) {
1477
1478           guardTimeInMs += 500;
1479
1480           if (guardTimeInMs > maxGuardTimeInMs) {
1481             outstandingWorkGuardTimerFired = true;
1482           }
1483         } else {
1484           guardTimeInMs = 0;
1485         }
1486
1487         hasOutstandingWork = hasOutStandingWork();
1488
1489       }
1490
1491       long opTime = System.currentTimeMillis() - startTimeInMs;
1492
1493       LOG.info(AaiUiMsgs.ALL_TRANSACTIONS_RESOLVED, String.valueOf(totalResolveTime),
1494           String.valueOf(totalLinksRetrieved.get()), String.valueOf(opTime));
1495
1496     } catch (Exception exc) {
1497       LOG.error(AaiUiMsgs.VISUALIZATION_OUTPUT_ERROR, exc.getMessage());
1498     }
1499
1500   }
1501
1502   /**
1503    * Verify outbound neighbors.
1504    */
1505   private void verifyOutboundNeighbors() {
1506
1507     for (ActiveInventoryNode srcNode : nodeCache.values()) {
1508
1509       for (String targetNodeId : srcNode.getOutboundNeighbors()) {
1510
1511         ActiveInventoryNode targetNode = nodeCache.get(targetNodeId);
1512
1513         if (targetNode != null && srcNode.getNodeId() != null) {
1514
1515           targetNode.addInboundNeighbor(srcNode.getNodeId());
1516
1517           if (this.visualizationConfigs.makeAllNeighborsBidirectional()) {
1518             targetNode.addOutboundNeighbor(srcNode.getNodeId());
1519           }
1520
1521         }
1522
1523       }
1524
1525     }
1526
1527   }
1528
1529   /**
1530    * Evaluate node depths.
1531    */
1532   private void evaluateNodeDepths() {
1533
1534     int numChanged = -1;
1535     int numAttempts = 0;
1536
1537     while (numChanged != 0) {
1538
1539       numChanged = 0;
1540       numAttempts++;
1541
1542       for (ActiveInventoryNode srcNode : nodeCache.values()) {
1543
1544         if (srcNode.getState() == NodeProcessingState.INIT) {
1545
1546           /*
1547            * this maybe the only state that we don't want to to process the node depth on, because
1548            * typically it won't have any valid fields set, and it may remain in a partial state
1549            * until we have processed the self-link.
1550            */
1551
1552           continue;
1553
1554         }
1555
1556         for (String targetNodeId : srcNode.getOutboundNeighbors()) {
1557           ActiveInventoryNode targetNode = nodeCache.get(targetNodeId);
1558
1559           if (targetNode != null) {
1560
1561             if (targetNode.changeDepth(srcNode.getNodeDepth() + 1)) {
1562               numChanged++;
1563             }
1564           }
1565         }
1566
1567         for (String targetNodeId : srcNode.getInboundNeighbors()) {
1568           ActiveInventoryNode targetNode = nodeCache.get(targetNodeId);
1569
1570           if (targetNode != null) {
1571
1572             if (targetNode.changeDepth(srcNode.getNodeDepth() + 1)) {
1573               numChanged++;
1574             }
1575           }
1576         }
1577       }
1578
1579       if (numAttempts >= MAX_DEPTH_EVALUATION_ATTEMPTS) {
1580         LOG.info(AaiUiMsgs.MAX_EVALUATION_ATTEMPTS_EXCEEDED);
1581         return;
1582       }
1583
1584     }
1585
1586     if (LOG.isDebugEnabled()) {
1587       if (numAttempts > 0) {
1588         LOG.debug(AaiUiMsgs.DEBUG_GENERIC, 
1589             "Evaluate node depths completed in " + numAttempts + " attempts");
1590       } else {
1591         LOG.debug(AaiUiMsgs.DEBUG_GENERIC, 
1592             "Evaluate node depths completed in 0 attempts because all nodes at correct depth");
1593       }
1594     }
1595
1596   }
1597
1598
1599   /**
1600    * Gets the entity type primary key name.
1601    *
1602    * @param entityType the entity type
1603    * @return the entity type primary key name
1604    */
1605
1606   
1607   private String getEntityTypePrimaryKeyName(String entityType) {
1608
1609     if (entityType == null) {
1610       LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE, "node primary key"
1611           + " name because entity type is null");
1612       return null;
1613     }
1614
1615     OxmEntityDescriptor descriptor = oxmEntityLookup.getEntityDescriptors().get(entityType);
1616
1617     if (descriptor == null) {
1618       LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE, "oxm entity"
1619           + " descriptor for entityType = " + entityType);
1620       return null;
1621     }
1622
1623     List<String> pkeyNames = descriptor.getPrimaryKeyAttributeNames();
1624
1625     if (pkeyNames == null || pkeyNames.size() == 0) {
1626       LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE, "node primary"
1627           + " key because descriptor primary key names is empty");
1628       return null;
1629     }
1630
1631     return NodeUtils.concatArray(pkeyNames, "/");
1632
1633   }
1634
1635 }