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