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