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