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