Adding back-end support for UI filters
[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 =
80       LoggerFactory.getInstance().getLogger(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,
256                   "decodeComplexAttributeGroup()," + " 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
304                * node 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,
316                   "Failed to add child to parent for child = " + entity.toString());
317             }
318
319             if (!addNodeQueryParams(newNode)) {
320               LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE_NODE_ID,
321                   "Error determining node id and key for node = " + newNode.dumpNodeTree(true)
322                       + " skipping relationship processing");
323               newNode.changeState(NodeProcessingState.ERROR,
324                   NodeProcessingAction.NODE_IDENTITY_ERROR);
325               return false;
326             } else {
327
328               newNode.changeState(NodeProcessingState.NEIGHBORS_UNPROCESSED,
329                   NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_OK);
330
331             }
332
333
334             /*
335              * Order matters for the query params. We need to set the parent ones before the child
336              * node
337              */
338
339             String selfLinkQuery =
340                 aaiProvider.getGenericQueryForSelfLink(entityType, newNode.getQueryParams());
341
342             /**
343              * <li>get the self-link
344              * <li>add it to the new node
345              * <li>generate node id
346              * <li>add node to node cache
347              * <li>add node id to parent outbound links list
348              * <li>process node children (should be automatic) (but don't query and resolve
349              * self-link as we already have all the data)
350              */
351
352             SelfLinkDeterminationTransaction txn = new SelfLinkDeterminationTransaction();
353
354             txn.setQueryString(selfLinkQuery);
355             txn.setNewNode(newNode);
356             txn.setParentNodeId(ain.getNodeId());
357             aaiWorkOnHand.incrementAndGet();
358             supplyAsync(new PerformSelfLinkDeterminationTask(txn, null, aaiProvider),
359                 aaiExecutorService).whenComplete((nodeTxn, error) -> {
360                   aaiWorkOnHand.decrementAndGet();
361                   if (error != null) {
362                     LOG.error(AaiUiMsgs.SELF_LINK_DETERMINATION_FAILED_GENERIC, selfLinkQuery);
363                   } else {
364
365                     OperationResult opResult = nodeTxn.getOpResult();
366
367                     ActiveInventoryNode newChildNode = txn.getNewNode();
368
369                     if (opResult != null && opResult.wasSuccessful()) {
370
371                       if (opResult.isResolvedLinkFailure()) {
372                         numFailedLinkResolve.incrementAndGet();
373                       }
374
375                       if (opResult.isResolvedLinkFromCache()) {
376                         numSuccessfulLinkResolveFromCache.incrementAndGet();
377                       }
378
379                       if (opResult.isResolvedLinkFromServer()) {
380                         numSuccessfulLinkResolveFromFromServer.incrementAndGet();
381                       }
382
383                       /*
384                        * extract the self-link from the operational result.
385                        */
386
387                       Collection<JsonNode> entityLinks = new ArrayList<JsonNode>();
388                       JsonNode genericQueryResult = null;
389                       try {
390                         genericQueryResult =
391                             NodeUtils.convertJsonStrToJsonNode(nodeTxn.getOpResult().getResult());
392                       } catch (Exception exc) {
393                         LOG.error(AaiUiMsgs.JSON_CONVERSION_ERROR, JsonNode.class.toString(),
394                             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,
405                             String.valueOf(entityLinks.size()));
406
407                       } else {
408                         selfLink = ((JsonNode) entityLinks.toArray()[0]).asText();
409                         selfLink = ActiveInventoryConfig.extractResourcePath(selfLink);
410
411                         newChildNode.setSelfLink(selfLink);
412                         newChildNode.setNodeId(NodeUtils.generateUniqueShaDigest(selfLink));
413
414                         String uri = NodeUtils.calculateEditAttributeUri(selfLink);
415                         if (uri != null) {
416                           newChildNode.addProperty(TierSupportUiConstants.URI_ATTR_NAME, uri);
417                         }
418
419                         ActiveInventoryNode parent = nodeCache.get(txn.getParentNodeId());
420
421                         if (parent != null) {
422                           parent.addOutboundNeighbor(newChildNode.getNodeId());
423                           newChildNode.addInboundNeighbor(parent.getNodeId());
424                         }
425
426                         newChildNode.setSelfLinkPendingResolve(false);
427                         newChildNode.setSelfLinkProcessed(true);
428
429                         newChildNode.changeState(NodeProcessingState.NEIGHBORS_UNPROCESSED,
430                             NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_OK);
431
432                         nodeCache.putIfAbsent(newChildNode.getNodeId(), newChildNode);
433
434                       }
435
436                     } else {
437                       LOG.error(AaiUiMsgs.SELF_LINK_RETRIEVAL_FAILED, txn.getQueryString(),
438                           String.valueOf(nodeTxn.getOpResult().getResultCode()),
439                           nodeTxn.getOpResult().getResult());
440                       newChildNode.setSelflinkRetrievalFailure(true);
441                       newChildNode.setSelfLinkProcessed(true);
442                       newChildNode.setSelfLinkPendingResolve(false);
443
444                       newChildNode.changeState(NodeProcessingState.ERROR,
445                           NodeProcessingAction.SELF_LINK_DETERMINATION_ERROR);
446
447                     }
448
449                   }
450
451                 });
452
453           }
454
455           return true;
456
457         } else {
458           LOG.error(AaiUiMsgs.UNHANDLED_OBJ_TYPE_FOR_ENTITY_TYPE, entityType);
459         }
460
461       }
462     } catch (Exception exc) {
463       LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR,
464           "Exception caught while" + " decoding complex attribute group - " + exc.getMessage());
465     }
466
467     return false;
468
469   }
470
471   /**
472    * Process self link response.
473    *
474    * @param nodeId the node id
475    */
476   private void processSelfLinkResponse(String nodeId) {
477
478     if (nodeId == null) {
479       LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR,
480           "Cannot process self link" + " response because nodeId is null");
481       return;
482     }
483
484     ActiveInventoryNode ain = nodeCache.get(nodeId);
485
486     if (ain == null) {
487       LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR,
488           "Cannot process self link response" + " because can't find node for id = " + nodeId);
489       return;
490     }
491
492     JsonNode jsonNode = null;
493
494     try {
495       jsonNode = mapper.readValue(ain.getOpResult().getResult(), JsonNode.class);
496     } catch (Exception exc) {
497       LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to marshal json"
498           + " response str into JsonNode with error, " + exc.getLocalizedMessage());
499       ain.changeState(NodeProcessingState.ERROR,
500           NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_ERROR);
501       return;
502     }
503
504     if (jsonNode == null) {
505       LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR,
506           "Failed to parse json node str." + " Parse resulted a null value.");
507       ain.changeState(NodeProcessingState.ERROR,
508           NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_ERROR);
509       return;
510     }
511
512     Iterator<Entry<String, JsonNode>> fieldNames = jsonNode.fields();
513     Entry<String, JsonNode> field = null;
514
515     RelationshipList relationshipList = null;
516
517     while (fieldNames.hasNext()) {
518
519       field = fieldNames.next();
520       String fieldName = field.getKey();
521
522       if ("relationship-list".equals(fieldName)) {
523
524         try {
525           relationshipList = mapper.readValue(field.getValue().toString(), RelationshipList.class);
526
527           if (relationshipList != null) {
528             ain.addRelationshipList(relationshipList);
529           }
530
531         } catch (Exception exc) {
532           LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to parse relationship-list"
533               + " attribute. Parse resulted in error, " + exc.getLocalizedMessage());
534           ain.changeState(NodeProcessingState.ERROR,
535               NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_ERROR);
536           return;
537         }
538
539       } else {
540
541         JsonNode nodeValue = field.getValue();
542
543         if (nodeValue != null && nodeValue.isValueNode()) {
544
545           if (loader.getEntityDescriptor(fieldName) == null) {
546
547             /*
548              * entity property name is not an entity, thus we can add this property name and value
549              * to our property set
550              */
551
552             ain.addProperty(fieldName, nodeValue.asText());
553
554           }
555
556         } else {
557
558           if (nodeValue.isArray()) {
559
560             if (loader.getEntityDescriptor(fieldName) == null) {
561
562               /*
563                * entity property name is not an entity, thus we can add this property name and value
564                * to our property set
565                */
566
567               ain.addProperty(field.getKey(), nodeValue.toString());
568
569             }
570
571           } else {
572
573             ain.addComplexGroup(nodeValue);
574
575           }
576
577         }
578       }
579
580     }
581
582     String uri = NodeUtils.calculateEditAttributeUri(ain.getSelfLink());
583     if (uri != null) {
584       ain.addProperty(TierSupportUiConstants.URI_ATTR_NAME, uri);
585     }
586
587     /*
588      * We need a special behavior for intermediate entities from the REST model
589      * 
590      * Tenants are not top level entities, and when we want to visualization their children, we need
591      * to construct keys that include the parent entity query keys, the current entity type keys,
592      * and the child keys. We'll always have the current entity and children, but never the parent
593      * entity in the current (1707) REST data model.
594      * 
595      * We have two possible solutions:
596      * 
597      * 1) Try to use the custom-query approach to learn about the entity keys - this could be done,
598      * but it could be very expensive for large objects. When we do the first query to get a tenant,
599      * it will list all the in and out edges related to this entity, there is presently no way to
600      * filter this. But the approach could be made to work and it would be somewhat data-model
601      * driven, other than the fact that we have to first realize that the entity that is being
602      * searched for is not top-level entity. Once we have globally unique ids for resources this
603      * logic will not be needed and everything will be simpler. The only reason we are in this logic
604      * at all is to be able to calculate a url for the child entities so we can hash it to generate
605      * a globally unique id that can be safely used for the node.
606      * 
607      * *2* Extract the keys from the pathed self-link. This is a bad solution and I don't like it
608      * but it will be fast for all resource types, as the information is already encoded in the URI.
609      * When we get to a point where we switch to a better globally unique entity identity model,
610      * then a lot of the code being used to calculate an entity url to in-turn generate a
611      * deterministic globally unique id will disappear.
612      * 
613      * 
614      * right now we have the following:
615      * 
616      * - cloud-regions/cloud-region/{cloud-region-id}/{cloud-owner-id}/tenants/tenant/{tenant-id}
617      * 
618      */
619
620     /*
621      * For all entity types use the self-link extraction method to be consistent. Once we have a
622      * globally unique identity mechanism for entities, this logic can be revisited.
623      */
624     ain.clearQueryParams();
625     ain.addQueryParams(extractQueryParamsFromSelfLink(ain.getSelfLink()));
626
627     ain.changeState(NodeProcessingState.NEIGHBORS_UNPROCESSED,
628         NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_OK);
629
630   }
631
632   /**
633    * Perform self link resolve.
634    *
635    * @param nodeId the node id
636    */
637   private void performSelfLinkResolve(String nodeId) {
638
639     if (nodeId == null) {
640       LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR,
641           "Resolve of self-link" + " has been skipped because provided nodeId is null");
642       return;
643     }
644
645     ActiveInventoryNode ain = nodeCache.get(nodeId);
646
647     if (ain == null) {
648       LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Failed to find node with id, " + nodeId
649           + ", from node cache. Resolve self-link method has been skipped.");
650       return;
651     }
652
653     if (!ain.isSelfLinkPendingResolve()) {
654
655       ain.setSelfLinkPendingResolve(true);
656
657       // kick off async self-link resolution
658
659       if (LOG.isDebugEnabled()) {
660         LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
661             "About to process node in SELF_LINK_UNPROCESSED State, link = " + ain.getSelfLink());
662       }
663
664       numLinksDiscovered.incrementAndGet();
665
666       String depthModifier = DEPTH_ALL_MODIFIER;
667
668       /*
669        * If the current node is the search target, we want to see everything the node has to offer
670        * from the self-link and not filter it to a single node.
671        */
672
673       if (shallowEntities.contains(ain.getEntityType()) && !ain.isRootNode()) {
674         depthModifier = NODES_ONLY_MODIFIER;
675       }
676
677       NodeProcessingTransaction txn = new NodeProcessingTransaction();
678       txn.setProcessingNode(ain);
679       txn.setRequestParameters(depthModifier);
680       aaiWorkOnHand.incrementAndGet();
681       supplyAsync(new PerformNodeSelfLinkProcessingTask(txn, depthModifier, aaiProvider, aaiConfig),
682           aaiExecutorService).whenComplete((nodeTxn, error) -> {
683             aaiWorkOnHand.decrementAndGet();
684             if (error != null) {
685
686               /*
687                * an error processing the self link should probably result in the node processing
688                * state shifting to ERROR
689                */
690
691               nodeTxn.getProcessingNode().setSelflinkRetrievalFailure(true);
692
693               nodeTxn.getProcessingNode().changeState(NodeProcessingState.ERROR,
694                   NodeProcessingAction.SELF_LINK_RESOLVE_ERROR);
695
696               nodeTxn.getProcessingNode().setSelfLinkPendingResolve(false);
697
698             } else {
699
700               totalLinksRetrieved.incrementAndGet();
701
702               OperationResult opResult = nodeTxn.getOpResult();
703
704               if (opResult != null && opResult.wasSuccessful()) {
705
706                 if (opResult.isResolvedLinkFailure()) {
707                   numFailedLinkResolve.incrementAndGet();
708                 }
709
710                 if (opResult.isResolvedLinkFromCache()) {
711                   numSuccessfulLinkResolveFromCache.incrementAndGet();
712                 }
713
714                 if (opResult.isResolvedLinkFromServer()) {
715                   numSuccessfulLinkResolveFromFromServer.incrementAndGet();
716                 }
717
718                 // success path
719                 nodeTxn.getProcessingNode().setOpResult(opResult);
720                 nodeTxn.getProcessingNode().changeState(
721                     NodeProcessingState.SELF_LINK_RESPONSE_UNPROCESSED,
722                     NodeProcessingAction.SELF_LINK_RESOLVE_OK);
723
724                 nodeTxn.getProcessingNode().setSelfLinkProcessed(true);
725                 nodeTxn.getProcessingNode().setSelfLinkPendingResolve(false);
726
727               } else {
728                 LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR,
729                     "Self Link retrieval for link," + txn.getSelfLinkWithModifiers()
730                         + ", failed with error code," + nodeTxn.getOpResult().getResultCode()
731                         + ", and message," + nodeTxn.getOpResult().getResult());
732
733                 nodeTxn.getProcessingNode().setSelflinkRetrievalFailure(true);
734                 nodeTxn.getProcessingNode().setSelfLinkProcessed(true);
735
736                 nodeTxn.getProcessingNode().changeState(NodeProcessingState.ERROR,
737                     NodeProcessingAction.SELF_LINK_RESOLVE_ERROR);
738
739                 nodeTxn.getProcessingNode().setSelfLinkPendingResolve(false);
740
741               }
742             }
743
744           });
745
746     }
747
748   }
749
750
751   /**
752    * Process neighbors.
753    *
754    * @param nodeId the node id
755    */
756   private void processNeighbors(String nodeId) {
757
758     if (nodeId == null) {
759       LOG.error(AaiUiMsgs.SELF_LINK_PROCESS_NEIGHBORS_ERROR,
760           "Failed to process" + " neighbors because nodeId is null.");
761       return;
762     }
763
764     ActiveInventoryNode ain = nodeCache.get(nodeId);
765
766     if (ain == null) {
767       LOG.error(AaiUiMsgs.SELF_LINK_PROCESS_NEIGHBORS_ERROR, "Failed to process"
768           + " neighbors because node could not be found in nodeCache with id, " + nodeId);
769       return;
770     }
771
772     /*
773      * process complex attribute and relationships
774      */
775
776     boolean neighborsProcessedSuccessfully = true;
777
778     for (JsonNode n : ain.getComplexGroups()) {
779       neighborsProcessedSuccessfully &= decodeComplexAttributeGroup(ain, n);
780     }
781
782     for (RelationshipList relationshipList : ain.getRelationshipLists()) {
783       neighborsProcessedSuccessfully &= addSelfLinkRelationshipChildren(ain, relationshipList);
784     }
785
786
787     if (neighborsProcessedSuccessfully) {
788       ain.changeState(NodeProcessingState.READY, NodeProcessingAction.NEIGHBORS_PROCESSED_OK);
789     } else {
790       ain.changeState(NodeProcessingState.ERROR, NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
791     }
792
793
794     /*
795      * If neighbors fail to process, there is already a call to change the state within the
796      * relationship and neighbor processing functions.
797      */
798
799   }
800
801   /**
802    * Find and mark root node.
803    *
804    * @param queryParams the query params
805    * @return true, if successful
806    */
807   private boolean findAndMarkRootNode(QueryParams queryParams) {
808
809     for (ActiveInventoryNode cacheNode : nodeCache.values()) {
810
811       if (queryParams.getSearchTargetNodeId().equals(cacheNode.getNodeId())) {
812         cacheNode.setNodeDepth(0);
813         cacheNode.setRootNode(true);
814         LOG.info(AaiUiMsgs.ROOT_NODE_DISCOVERED, queryParams.getSearchTargetNodeId());
815         return true;
816       }
817     }
818
819     return false;
820
821   }
822
823   /**
824    * Process current node states.
825    *
826    * @param rootNodeDiscovered the root node discovered
827    */
828   private void processCurrentNodeStates(boolean rootNodeDiscovered) {
829     /*
830      * Force an evaluation of node depths before determining if we should limit state-based
831      * traversal or processing.
832      */
833     if (rootNodeDiscovered) {
834       evaluateNodeDepths();
835     }
836
837     for (ActiveInventoryNode cacheNode : nodeCache.values()) {
838
839       if (LOG.isDebugEnabled()) {
840         LOG.debug(AaiUiMsgs.DEBUG_GENERIC, "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 = -- " + cacheNode.getState()
881                       + " 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,
946                   "Failed to parse" + " 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, "Unexpected array type with a key = " + fieldName);
960           }
961         } else if (fieldValue.isValueNode()) {
962           if (loader.getEntityDescriptor(field.getKey()) == null) {
963             /*
964              * property key is not an entity type, add it to our property set.
965              */
966             targetNode.addProperty(field.getKey(), fieldValue.asText());
967           }
968
969         }
970       }
971
972     } else if (attributeGroup.isArray()) {
973       if (LOG.isDebugEnabled()) {
974         LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
975             "Unexpected array type for attributeGroup = " + attributeGroup);
976       }
977     } else if (attributeGroup.isValueNode()) {
978       if (LOG.isDebugEnabled()) {
979         LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
980             "Unexpected value type for attributeGroup = " + attributeGroup);
981       }
982     }
983
984     return true;
985   }
986
987   public int getNumSuccessfulLinkResolveFromCache() {
988     return numSuccessfulLinkResolveFromCache.get();
989   }
990
991   public int getNumSuccessfulLinkResolveFromFromServer() {
992     return numSuccessfulLinkResolveFromFromServer.get();
993   }
994
995   public int getNumFailedLinkResolve() {
996     return numFailedLinkResolve.get();
997   }
998
999   public InlineMessage getInlineMessage() {
1000     return inlineMessage;
1001   }
1002
1003   public void setInlineMessage(InlineMessage inlineMessage) {
1004     this.inlineMessage = inlineMessage;
1005   }
1006
1007   public void setMaxSelfLinkTraversalDepth(int depth) {
1008     this.maxSelfLinkTraversalDepth = depth;
1009   }
1010
1011   public int getMaxSelfLinkTraversalDepth() {
1012     return this.maxSelfLinkTraversalDepth;
1013   }
1014
1015   public ConcurrentHashMap<String, ActiveInventoryNode> getNodeCache() {
1016     return nodeCache;
1017   }
1018
1019   /**
1020    * Gets the relationship primary key values.
1021    *
1022    * @param r the r
1023    * @param entityType the entity type
1024    * @param pkeyNames the pkey names
1025    * @return the relationship primary key values
1026    */
1027   private String getRelationshipPrimaryKeyValues(Relationship r, String entityType,
1028       List<String> pkeyNames) {
1029
1030     StringBuilder sb = new StringBuilder(64);
1031
1032     if (pkeyNames.size() > 0) {
1033       String primaryKey = extractKeyValueFromRelationData(r, entityType + "." + pkeyNames.get(0));
1034       if (primaryKey != null) {
1035
1036         sb.append(primaryKey);
1037
1038       } else {
1039         // this should be a fatal error because unless we can
1040         // successfully retrieve all the expected keys we'll end up
1041         // with a garbage node
1042         LOG.error(AaiUiMsgs.EXTRACTION_ERROR, "ERROR: Failed to extract" + " keyName, " + entityType
1043             + "." + pkeyNames.get(0) + ", 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, " + entityType
1057               + "." + pkeyNames.get(i) + ", from relationship data, " + r.toString());
1058           return null;
1059         }
1060       }
1061
1062       return sb.toString();
1063
1064     }
1065
1066     return null;
1067
1068   }
1069
1070   /**
1071    * Extract key value from relation data.
1072    *
1073    * @param r the r
1074    * @param keyName the key name
1075    * @return the string
1076    */
1077   private String extractKeyValueFromRelationData(Relationship r, String keyName) {
1078
1079     RelationshipData[] rdList = r.getRelationshipData();
1080
1081     for (RelationshipData relData : rdList) {
1082
1083       if (relData.getRelationshipKey().equals(keyName)) {
1084         return relData.getRelationshipValue();
1085       }
1086     }
1087
1088     return null;
1089   }
1090
1091   /**
1092    * Determine node id and key.
1093    *
1094    * @param ain the ain
1095    * @return true, if successful
1096    */
1097   private boolean addNodeQueryParams(ActiveInventoryNode ain) {
1098
1099     if (ain == null) {
1100       LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE_NODE_ID, "ActiveInventoryNode is null");
1101       return false;
1102     }
1103
1104     List<String> pkeyNames =
1105         loader.getEntityDescriptor(ain.getEntityType()).getPrimaryKeyAttributeName();
1106
1107     if (pkeyNames == null || pkeyNames.size() == 0) {
1108       LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE_NODE_ID, "Primary key names is empty");
1109       return false;
1110     }
1111
1112     StringBuilder sb = new StringBuilder(64);
1113
1114     if (pkeyNames.size() > 0) {
1115       String primaryKey = ain.getProperties().get(pkeyNames.get(0));
1116       if (primaryKey != null) {
1117         sb.append(primaryKey);
1118       } else {
1119         // this should be a fatal error because unless we can
1120         // successfully retrieve all the expected keys we'll end up
1121         // with a garbage node
1122         LOG.error(AaiUiMsgs.EXTRACTION_ERROR,
1123             "ERROR: Failed to extract keyName, " + pkeyNames.get(0) + ", from entity properties");
1124         return false;
1125       }
1126
1127       for (int i = 1; i < pkeyNames.size(); i++) {
1128
1129         String kv = ain.getProperties().get(pkeyNames.get(i));
1130         if (kv != null) {
1131           sb.append("/").append(kv);
1132         } else {
1133           // this should be a fatal error because unless we can
1134           // successfully retrieve all the expected keys we'll end up
1135           // with a garbage node
1136           LOG.error(AaiUiMsgs.EXTRACTION_ERROR,
1137               "ERROR: Failed to extract keyName, " + pkeyNames.get(i) + ", from entity properties");
1138           return false;
1139         }
1140       }
1141
1142       /*
1143        * final String nodeId = NodeUtils.generateUniqueShaDigest(ain.getEntityType(),
1144        * NodeUtils.concatArray(pkeyNames, "/"), sb.toString());
1145        */
1146
1147       // ain.setNodeId(nodeId);
1148       ain.setPrimaryKeyName(NodeUtils.concatArray(pkeyNames, "/"));
1149       ain.setPrimaryKeyValue(sb.toString());
1150
1151       if (ain.getEntityType() != null && ain.getPrimaryKeyName() != null
1152           && ain.getPrimaryKeyValue() != null) {
1153         ain.addQueryParam(
1154             ain.getEntityType() + "." + ain.getPrimaryKeyName() + ":" + ain.getPrimaryKeyValue());
1155       }
1156       return true;
1157
1158     }
1159
1160     return false;
1161
1162   }
1163
1164   /**
1165    * Adds the self link relationship children.
1166    *
1167    * @param processingNode the processing node
1168    * @param relationshipList the relationship list
1169    * @return true, if successful
1170    */
1171   private boolean addSelfLinkRelationshipChildren(ActiveInventoryNode processingNode,
1172       RelationshipList relationshipList) {
1173
1174     if (relationshipList == null) {
1175       LOG.debug(AaiUiMsgs.DEBUG_GENERIC, "No relationships added to parent node = "
1176           + processingNode.getNodeId() + " because relationshipList is empty");
1177       processingNode.changeState(NodeProcessingState.ERROR,
1178           NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
1179       return false;
1180     }
1181
1182     OxmModelLoader modelLoader = OxmModelLoader.getInstance();
1183
1184     Relationship[] relationshipArray = relationshipList.getRelationshipList();
1185     OxmEntityDescriptor descriptor = null;
1186
1187     if (relationshipArray != null) {
1188
1189       ActiveInventoryNode newNode = null;
1190       String resourcePath = null;
1191
1192       for (Relationship r : relationshipArray) {
1193
1194         resourcePath = ActiveInventoryConfig.extractResourcePath(r.getRelatedLink());
1195
1196         String nodeId = NodeUtils.generateUniqueShaDigest(resourcePath);
1197
1198         if (nodeId == null) {
1199
1200           LOG.error(AaiUiMsgs.SKIPPING_RELATIONSHIP, r.toString());
1201           processingNode.changeState(NodeProcessingState.ERROR,
1202               NodeProcessingAction.NODE_IDENTITY_ERROR);
1203           return false;
1204         }
1205
1206         newNode = new ActiveInventoryNode();
1207
1208         String entityType = r.getRelatedTo();
1209
1210         if (r.getRelationshipData() != null) {
1211           for (RelationshipData rd : r.getRelationshipData()) {
1212             newNode.addQueryParam(rd.getRelationshipKey() + ":" + rd.getRelationshipValue());
1213           }
1214         }
1215
1216         descriptor = modelLoader.getEntityDescriptor(r.getRelatedTo());
1217
1218         newNode.setNodeId(nodeId);
1219         newNode.setEntityType(entityType);
1220         newNode.setSelfLink(resourcePath);
1221
1222         processingNode.addOutboundNeighbor(nodeId);
1223
1224         if (descriptor != null) {
1225
1226           List<String> pkeyNames = descriptor.getPrimaryKeyAttributeName();
1227
1228           newNode.changeState(NodeProcessingState.SELF_LINK_UNRESOLVED,
1229               NodeProcessingAction.SELF_LINK_SET);
1230
1231           newNode.setPrimaryKeyName(NodeUtils.concatArray(pkeyNames, "/"));
1232
1233           String primaryKeyValues = getRelationshipPrimaryKeyValues(r, entityType, pkeyNames);
1234           newNode.setPrimaryKeyValue(primaryKeyValues);
1235
1236         } else {
1237
1238           LOG.error(AaiUiMsgs.VISUALIZATION_OUTPUT_ERROR,
1239               "Failed to parse entity because OXM descriptor could not be found for type = "
1240                   + r.getRelatedTo());
1241
1242           newNode.changeState(NodeProcessingState.ERROR,
1243               NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
1244
1245         }
1246
1247         if (nodeCache.putIfAbsent(nodeId, newNode) != null) {
1248           if (LOG.isDebugEnabled()) {
1249             LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
1250                 "Failed to add node to nodeCache because it already exists.  Node id = "
1251                     + newNode.getNodeId());
1252           }
1253         }
1254
1255       }
1256
1257     }
1258
1259     return true;
1260
1261   }
1262
1263   /**
1264    * Process initial state.
1265    *
1266    * @param nodeId the node id
1267    */
1268   private void processInitialState(String nodeId) {
1269
1270     if (nodeId == null) {
1271       LOG.error(AaiUiMsgs.FAILED_TO_PROCESS_INITIAL_STATE, "Node id is null");
1272       return;
1273     }
1274
1275     ActiveInventoryNode cachedNode = nodeCache.get(nodeId);
1276
1277     if (cachedNode == null) {
1278       LOG.error(AaiUiMsgs.FAILED_TO_PROCESS_INITIAL_STATE,
1279           "Node cannot be" + " found for nodeId, " + nodeId);
1280       return;
1281     }
1282
1283     if (cachedNode.getSelfLink() == null) {
1284
1285       if (cachedNode.getNodeId() == null) {
1286
1287         /*
1288          * if the self link is null at the INIT state, which could be valid if this node is a
1289          * complex attribute group which didn't originate from a self-link, but in that situation
1290          * both the node id and node key should already be set.
1291          */
1292
1293         cachedNode.changeState(NodeProcessingState.ERROR, NodeProcessingAction.NODE_IDENTITY_ERROR);
1294
1295       }
1296
1297       if (cachedNode.getNodeId() != null) {
1298
1299         /*
1300          * This should be the success path branch if the self-link is not set
1301          */
1302
1303         cachedNode.changeState(NodeProcessingState.SELF_LINK_RESPONSE_UNPROCESSED,
1304             NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_OK);
1305
1306       }
1307
1308     } else {
1309
1310       if (cachedNode.hasResolvedSelfLink()) {
1311         LOG.error(AaiUiMsgs.INVALID_RESOLVE_STATE_DURING_INIT);
1312         cachedNode.changeState(NodeProcessingState.ERROR,
1313             NodeProcessingAction.UNEXPECTED_STATE_TRANSITION);
1314       } else {
1315         cachedNode.changeState(NodeProcessingState.SELF_LINK_UNRESOLVED,
1316             NodeProcessingAction.SELF_LINK_SET);
1317       }
1318     }
1319   }
1320
1321   /**
1322    * Process skeleton node.
1323    *
1324    * @param skeletonNode the skeleton node
1325    * @param queryParams the query params
1326    */
1327   private void processSearchableEntity(SearchableEntity searchTargetEntity,
1328       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(newNode.getEntityType() + "." + newNode.getPrimaryKeyName() + ":"
1350           + 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,
1443             contextIdStr + " - Failed to" + " 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 we'll need
1465        * to decouple the visualization processing from the main thread so it can continue to process
1466        * while the main thread is waiting on for count-down-latch gate to open. This may also be
1467        * easier once we move to the 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,
1625           "node primary key" + " 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,
1633           "oxm entity" + " 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,
1641           "node primary" + " key because descriptor primary key names is empty");
1642       return null;
1643     }
1644
1645     return NodeUtils.concatArray(pkeyNames, "/");
1646
1647   }
1648
1649 }