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