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