Adding UI extensibility
[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.cl.api.Logger;
40 import org.onap.aai.cl.eelf.LoggerFactory;
41 import org.onap.aai.restclient.client.OperationResult;
42 import org.onap.aai.sparky.config.oxm.OxmEntityDescriptor;
43 import org.onap.aai.sparky.config.oxm.OxmEntityLookup;
44 import org.onap.aai.sparky.config.oxm.OxmModelLoader;
45 import org.onap.aai.sparky.dal.ActiveInventoryAdapter;
46 import org.onap.aai.sparky.dal.aai.config.ActiveInventoryConfig;
47 import org.onap.aai.sparky.logging.AaiUiMsgs;
48 import org.onap.aai.sparky.sync.entity.SearchableEntity;
49 import org.onap.aai.sparky.util.NodeUtils;
50 import org.onap.aai.sparky.viewandinspect.config.TierSupportUiConstants;
51 import org.onap.aai.sparky.viewandinspect.config.VisualizationConfigs;
52 import org.onap.aai.sparky.viewandinspect.entity.ActiveInventoryNode;
53 import org.onap.aai.sparky.viewandinspect.entity.InlineMessage;
54 import org.onap.aai.sparky.viewandinspect.entity.NodeProcessingTransaction;
55 import org.onap.aai.sparky.viewandinspect.entity.QueryParams;
56 import org.onap.aai.sparky.viewandinspect.entity.Relationship;
57 import org.onap.aai.sparky.viewandinspect.entity.RelationshipData;
58 import org.onap.aai.sparky.viewandinspect.entity.RelationshipList;
59 import org.onap.aai.sparky.viewandinspect.entity.SelfLinkDeterminationTransaction;
60 import org.onap.aai.sparky.viewandinspect.enumeration.NodeProcessingAction;
61 import org.onap.aai.sparky.viewandinspect.enumeration.NodeProcessingState;
62 import org.onap.aai.sparky.viewandinspect.task.PerformNodeSelfLinkProcessingTask;
63 import org.onap.aai.sparky.viewandinspect.task.PerformSelfLinkDeterminationTask;
64
65 import com.fasterxml.jackson.annotation.JsonInclude.Include;
66 import com.fasterxml.jackson.databind.JsonNode;
67 import com.fasterxml.jackson.databind.ObjectMapper;
68 import com.fasterxml.jackson.databind.PropertyNamingStrategy;
69
70 /**
71  * The Class SelfLinkNodeCollector.
72  */
73 public class VisualizationContext {
74
75   private static final int MAX_DEPTH_EVALUATION_ATTEMPTS = 100;
76   private static final String DEPTH_ALL_MODIFIER = "?depth=all";
77   private static final String NODES_ONLY_MODIFIER = "?nodes-only";
78   private static final String SERVICE_INSTANCE = "service-instance";
79
80   private static final Logger LOG =
81       LoggerFactory.getInstance().getLogger(VisualizationContext.class);
82   private final ActiveInventoryAdapter aaiAdapter;
83
84   private int maxSelfLinkTraversalDepth;
85   private AtomicInteger numLinksDiscovered;
86   private AtomicInteger numSuccessfulLinkResolveFromCache;
87   private AtomicInteger numSuccessfulLinkResolveFromFromServer;
88   private AtomicInteger numFailedLinkResolve;
89   private AtomicInteger nodeIntegrityWorkOnHand;
90   private AtomicInteger aaiWorkOnHand;
91
92   private ActiveInventoryConfig aaiConfig;
93   private VisualizationConfigs visualizationConfigs;
94   private List<String> shallowEntities;
95
96   private AtomicInteger totalLinksRetrieved;
97
98   private final long contextId;
99   private final String contextIdStr;
100
101   private OxmModelLoader loader;
102   private ObjectMapper mapper;
103   private InlineMessage inlineMessage = null;
104
105   private ExecutorService tabularExecutorService;
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, ActiveInventoryAdapter aaiAdapter,
122       ExecutorService tabularExecutorService, ExecutorService aaiExecutorService,
123       VisualizationConfigs visualizationConfigs) throws Exception {
124
125     this.contextId = contextId;
126     this.contextIdStr = "[Context-Id=" + contextId + "]";
127     this.aaiAdapter = aaiAdapter;
128     this.tabularExecutorService = tabularExecutorService;
129     this.aaiExecutorService = aaiExecutorService;
130     this.visualizationConfigs = visualizationConfigs;
131
132     this.nodeCache = new ConcurrentHashMap<String, ActiveInventoryNode>();
133     this.numLinksDiscovered = new AtomicInteger(0);
134     this.totalLinksRetrieved = new AtomicInteger(0);
135     this.numSuccessfulLinkResolveFromCache = new AtomicInteger(0);
136     this.numSuccessfulLinkResolveFromFromServer = new AtomicInteger(0);
137     this.numFailedLinkResolve = new AtomicInteger(0);
138     this.nodeIntegrityWorkOnHand = new AtomicInteger(0);
139     this.aaiWorkOnHand = new AtomicInteger(0);
140
141     this.aaiConfig = ActiveInventoryConfig.getConfig();
142     this.shallowEntities = aaiConfig.getAaiRestConfig().getShallowEntities();
143
144     this.maxSelfLinkTraversalDepth = this.visualizationConfigs.getMaxSelfLinkTraversalDepth();
145
146     this.mapper = new ObjectMapper();
147     mapper.setSerializationInclusion(Include.NON_EMPTY);
148     mapper.setPropertyNamingStrategy(new PropertyNamingStrategy.KebabCaseStrategy());
149   }
150
151   public long getContextId() {
152     return contextId;
153   }
154
155   /**
156    * A utility method for extracting all entity-type primary key values from a provided self-link
157    * and return a set of generic-query API keys.
158    * 
159    * @param parentEntityType
160    * @param link
161    * @return a list of key values that can be used for this entity with the AAI generic-query API
162    */
163   protected List<String> extractQueryParamsFromSelfLink(String link) {
164
165     List<String> queryParams = new ArrayList<String>();
166
167     if (link == null) {
168       LOG.error(AaiUiMsgs.QUERY_PARAM_EXTRACTION_ERROR, "self link is null");
169       return queryParams;
170     }
171
172     Map<String, OxmEntityDescriptor> entityDescriptors =
173         OxmEntityLookup.getInstance().getEntityDescriptors();
174
175     try {
176
177       URIBuilder urlBuilder = new URIBuilder(link);
178       String urlPath = urlBuilder.getPath();
179
180       OxmEntityDescriptor descriptor = null;
181       String[] urlPathElements = urlPath.split("/");
182       List<String> primaryKeyNames = null;
183       int index = 0;
184       String entityType = null;
185
186       while (index < urlPathElements.length) {
187
188         descriptor = entityDescriptors.get(urlPathElements[index]);
189
190         if (descriptor != null) {
191           entityType = urlPathElements[index];
192           primaryKeyNames = descriptor.getPrimaryKeyAttributeNames();
193
194           /*
195            * Make sure from what ever index we matched the parent entity-type on that we can extract
196            * additional path elements for the primary key values.
197            */
198
199           if (index + primaryKeyNames.size() < urlPathElements.length) {
200
201             for (String primaryKeyName : primaryKeyNames) {
202               index++;
203               queryParams.add(entityType + "." + primaryKeyName + ":" + urlPathElements[index]);
204             }
205           } else {
206             LOG.error(AaiUiMsgs.QUERY_PARAM_EXTRACTION_ERROR,
207                 "Could not extract query parametrs for entity-type = '" + entityType
208                     + "' from self-link = " + link);
209           }
210         }
211
212         index++;
213       }
214
215     } catch (URISyntaxException exc) {
216
217       LOG.error(AaiUiMsgs.QUERY_PARAM_EXTRACTION_ERROR,
218           "Error extracting query parameters from self-link = " + link + ". Error = "
219               + exc.getMessage());
220     }
221
222     return queryParams;
223
224   }
225
226   /**
227    * Decode complex attribute group.
228    *
229    * @param ain the ain
230    * @param attributeGroup the attribute group
231    * @return boolean indicating whether operation was successful (true), / failure(false).
232    */
233   public boolean decodeComplexAttributeGroup(ActiveInventoryNode ain, JsonNode attributeGroup) {
234
235     try {
236
237       Iterator<Entry<String, JsonNode>> entityArrays = attributeGroup.fields();
238       Entry<String, JsonNode> entityArray = null;
239
240       if (entityArrays == null) {
241         LOG.error(AaiUiMsgs.ATTRIBUTE_GROUP_FAILURE, attributeGroup.toString());
242         ain.changeState(NodeProcessingState.ERROR, NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
243         return false;
244       }
245
246       while (entityArrays.hasNext()) {
247
248         entityArray = entityArrays.next();
249
250         String entityType = entityArray.getKey();
251         JsonNode entityArrayObject = entityArray.getValue();
252
253         if (entityArrayObject.isArray()) {
254
255           Iterator<JsonNode> entityCollection = entityArrayObject.elements();
256           JsonNode entity = null;
257           while (entityCollection.hasNext()) {
258             entity = entityCollection.next();
259
260             if (LOG.isDebugEnabled()) {
261               LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
262                   "decodeComplexAttributeGroup()," + " entity = " + entity.toString());
263             }
264
265             /**
266              * Here's what we are going to do:
267              * 
268              * <li>In the ActiveInventoryNode, on construction maintain a collection of queryParams
269              * that is added to for the purpose of discovering parent->child hierarchies.
270              * 
271              * <li>When we hit this block of the code then we'll use the queryParams to feed the
272              * generic query to resolve the self-link asynchronously.
273              * 
274              * <li>Upon successful link determination, then and only then will we create a new node
275              * in the nodeCache and process the child
276              * 
277              */
278
279             ActiveInventoryNode newNode = new ActiveInventoryNode(this.visualizationConfigs);
280             newNode.setEntityType(entityType);
281
282             /*
283              * This is partially a lie because we actually don't have a self-link for complex nodes
284              * discovered in this way.
285              */
286             newNode.setSelfLinkProcessed(true);
287             newNode.changeState(NodeProcessingState.SELF_LINK_RESPONSE_UNPROCESSED,
288                 NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_OK);
289
290             /*
291              * copy parent query params into new child
292              */
293
294             if (SERVICE_INSTANCE.equals(entityType)) {
295
296               /*
297                * 1707 AAI has an issue being tracked with AAI-8932 where the generic-query cannot be
298                * resolved if all the service-instance path keys are provided. The query only works
299                * if only the service-instance key and valude are passed due to a historical reason.
300                * A fix is being worked on for 1707, and when it becomes available we can revert this
301                * small change.
302                */
303
304               newNode.clearQueryParams();
305
306             } else {
307
308               /*
309                * For all other entity-types we want to copy the parent query parameters into the new
310                * node query parameters.
311                */
312
313               for (String queryParam : ain.getQueryParams()) {
314                 newNode.addQueryParam(queryParam);
315               }
316
317             }
318
319
320             if (!addComplexGroupToNode(newNode, entity)) {
321               LOG.error(AaiUiMsgs.ATTRIBUTE_GROUP_FAILURE,
322                   "Failed to add child to parent for child = " + entity.toString());
323             }
324
325             if (!addNodeQueryParams(newNode)) {
326               LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE_NODE_ID,
327                   "Error determining node id and key for node = " + newNode.dumpNodeTree(true)
328                       + " skipping relationship processing");
329               newNode.changeState(NodeProcessingState.ERROR,
330                   NodeProcessingAction.NODE_IDENTITY_ERROR);
331               return false;
332             } else {
333
334               newNode.changeState(NodeProcessingState.NEIGHBORS_UNPROCESSED,
335                   NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_OK);
336
337             }
338
339
340             /*
341              * Order matters for the query params. We need to set the parent ones before the child
342              * node
343              */
344
345             String selfLinkQuery =
346                 aaiAdapter.getGenericQueryForSelfLink(entityType, newNode.getQueryParams());
347
348             /**
349              * <li>get the self-link
350              * <li>add it to the new node
351              * <li>generate node id
352              * <li>add node to node cache
353              * <li>add node id to parent outbound links list
354              * <li>process node children (should be automatic) (but don't query and resolve
355              * self-link as we already have all the data)
356              */
357
358             SelfLinkDeterminationTransaction txn = new SelfLinkDeterminationTransaction();
359
360             txn.setQueryString(selfLinkQuery);
361             txn.setNewNode(newNode);
362             txn.setParentNodeId(ain.getNodeId());
363             aaiWorkOnHand.incrementAndGet();
364             supplyAsync(new PerformSelfLinkDeterminationTask(txn, null, aaiAdapter),
365                 aaiExecutorService).whenComplete((nodeTxn, error) -> {
366                   aaiWorkOnHand.decrementAndGet();
367                   if (error != null) {
368                     LOG.error(AaiUiMsgs.SELF_LINK_DETERMINATION_FAILED_GENERIC, selfLinkQuery);
369                   } else {
370
371                     OperationResult opResult = nodeTxn.getOpResult();
372
373                     ActiveInventoryNode newChildNode = txn.getNewNode();
374
375                     if (opResult != null && opResult.wasSuccessful()) {
376
377                       if (!opResult.wasSuccessful()) {
378                         numFailedLinkResolve.incrementAndGet();
379                       }
380
381                       if (opResult.isFromCache()) {
382                         numSuccessfulLinkResolveFromCache.incrementAndGet();
383                       } else {
384                         numSuccessfulLinkResolveFromFromServer.incrementAndGet();
385                       }
386
387                       /*
388                        * extract the self-link from the operational result.
389                        */
390
391                       Collection<JsonNode> entityLinks = new ArrayList<JsonNode>();
392                       JsonNode genericQueryResult = null;
393                       try {
394                         genericQueryResult =
395                             NodeUtils.convertJsonStrToJsonNode(nodeTxn.getOpResult().getResult());
396                       } catch (Exception exc) {
397                         LOG.error(AaiUiMsgs.JSON_CONVERSION_ERROR, JsonNode.class.toString(),
398                             exc.getMessage());
399                       }
400
401                       NodeUtils.extractObjectsByKey(genericQueryResult, "resource-link",
402                           entityLinks);
403
404                       String selfLink = null;
405
406                       if (entityLinks.size() != 1) {
407
408                         LOG.error(AaiUiMsgs.SELF_LINK_DETERMINATION_FAILED_UNEXPECTED_LINKS,
409                             String.valueOf(entityLinks.size()));
410
411                       } else {
412                         selfLink = ((JsonNode) entityLinks.toArray()[0]).asText();
413                         selfLink = ActiveInventoryConfig.extractResourcePath(selfLink);
414
415                         newChildNode.setSelfLink(selfLink);
416                         newChildNode.setNodeId(NodeUtils.generateUniqueShaDigest(selfLink));
417
418                         String uri = NodeUtils.calculateEditAttributeUri(selfLink);
419                         if (uri != null) {
420                           newChildNode.addProperty(TierSupportUiConstants.URI_ATTR_NAME, uri);
421                         }
422
423                         ActiveInventoryNode parent = nodeCache.get(txn.getParentNodeId());
424
425                         if (parent != null) {
426                           parent.addOutboundNeighbor(newChildNode.getNodeId());
427                           newChildNode.addInboundNeighbor(parent.getNodeId());
428                         }
429
430                         newChildNode.setSelfLinkPendingResolve(false);
431                         newChildNode.setSelfLinkProcessed(true);
432                         newChildNode.changeState(NodeProcessingState.NEIGHBORS_UNPROCESSED,
433                             NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_OK);
434
435                         nodeCache.putIfAbsent(newChildNode.getNodeId(), newChildNode);
436
437                       }
438
439                     } else {
440                       LOG.error(AaiUiMsgs.SELF_LINK_RETRIEVAL_FAILED, txn.getQueryString(),
441                           String.valueOf(nodeTxn.getOpResult().getResultCode()),
442                           nodeTxn.getOpResult().getResult());
443                       newChildNode.setSelflinkRetrievalFailure(true);
444                       newChildNode.setSelfLinkProcessed(true);
445                       newChildNode.setSelfLinkPendingResolve(false);
446
447                       newChildNode.changeState(NodeProcessingState.ERROR,
448                           NodeProcessingAction.SELF_LINK_DETERMINATION_ERROR);
449
450                     }
451
452                   }
453
454                 });
455
456           }
457
458           return true;
459
460         } else {
461           LOG.error(AaiUiMsgs.UNHANDLED_OBJ_TYPE_FOR_ENTITY_TYPE, entityType);
462         }
463
464       }
465     } catch (Exception exc) {
466       LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR,
467           "Exception caught while" + " decoding complex attribute group - " + exc.getMessage());
468     }
469
470     return false;
471
472   }
473
474   /**
475    * Process self link response.
476    *
477    * @param nodeId the node id
478    */
479   private void processSelfLinkResponse(String nodeId) {
480
481     if (nodeId == null) {
482       LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR,
483           "Cannot process self link" + " response because nodeId is null");
484       return;
485     }
486
487     ActiveInventoryNode ain = nodeCache.get(nodeId);
488
489     if (ain == null) {
490       LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR,
491           "Cannot process self link response" + " because can't find node for id = " + nodeId);
492       return;
493     }
494
495     JsonNode jsonNode = null;
496
497     try {
498       jsonNode = mapper.readValue(ain.getOpResult().getResult(), JsonNode.class);
499     } catch (Exception exc) {
500       LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to marshal json"
501           + " response str into JsonNode with error, " + exc.getLocalizedMessage());
502       ain.changeState(NodeProcessingState.ERROR,
503           NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_ERROR);
504       return;
505     }
506
507     if (jsonNode == null) {
508       LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR,
509           "Failed to parse json node str." + " Parse resulted a null value.");
510       ain.changeState(NodeProcessingState.ERROR,
511           NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_ERROR);
512       return;
513     }
514
515     Iterator<Entry<String, JsonNode>> fieldNames = jsonNode.fields();
516     Entry<String, JsonNode> field = null;
517
518     RelationshipList relationshipList = null;
519
520     while (fieldNames.hasNext()) {
521
522       field = fieldNames.next();
523       String fieldName = field.getKey();
524
525       if ("relationship-list".equals(fieldName)) {
526
527         try {
528           relationshipList = mapper.readValue(field.getValue().toString(), RelationshipList.class);
529
530           if (relationshipList != null) {
531             ain.addRelationshipList(relationshipList);
532           }
533
534         } catch (Exception exc) {
535           LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to parse relationship-list"
536               + " attribute. Parse resulted in error, " + exc.getLocalizedMessage());
537           ain.changeState(NodeProcessingState.ERROR,
538               NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_ERROR);
539           return;
540         }
541
542       } else {
543
544         JsonNode nodeValue = field.getValue();
545
546         if (nodeValue != null && nodeValue.isValueNode()) {
547
548           if (OxmEntityLookup.getInstance().getEntityDescriptors().get(fieldName) == null) {
549
550             /*
551              * entity property name is not an entity, thus we can add this property name and value
552              * to our property set
553              */
554
555             ain.addProperty(fieldName, nodeValue.asText());
556
557           }
558
559         } else {
560
561           if (nodeValue.isArray()) {
562
563             if (OxmEntityLookup.getInstance().getEntityDescriptors().get(fieldName) == null) {
564
565               /*
566                * entity property name is not an entity, thus we can add this property name and value
567                * to our property set
568                */
569
570               ain.addProperty(field.getKey(), nodeValue.toString());
571
572             }
573
574           } else {
575
576             ain.addComplexGroup(nodeValue);
577
578           }
579
580         }
581       }
582
583     }
584
585     String uri = NodeUtils.calculateEditAttributeUri(ain.getSelfLink());
586     if (uri != null) {
587       ain.addProperty(TierSupportUiConstants.URI_ATTR_NAME, uri);
588     }
589
590     /*
591      * We need a special behavior for intermediate entities from the REST model
592      * 
593      * Tenants are not top level entities, and when we want to visualization their children, we need
594      * to construct keys that include the parent entity query keys, the current entity type keys,
595      * and the child keys. We'll always have the current entity and children, but never the parent
596      * entity in the current (1707) REST data model.
597      * 
598      * We have two possible solutions:
599      * 
600      * 1) Try to use the custom-query approach to learn about the entity keys - this could be done,
601      * but it could be very expensive for large objects. When we do the first query to get a tenant,
602      * it will list all the in and out edges related to this entity, there is presently no way to
603      * filter this. But the approach could be made to work and it would be somewhat data-model
604      * driven, other than the fact that we have to first realize that the entity that is being
605      * searched for is not top-level entity. Once we have globally unique ids for resources this
606      * logic will not be needed and everything will be simpler. The only reason we are in this logic
607      * at all is to be able to calculate a url for the child entities so we can hash it to generate
608      * a globally unique id that can be safely used for the node.
609      * 
610      * *2* Extract the keys from the pathed self-link. This is a bad solution and I don't like it
611      * but it will be fast for all resource types, as the information is already encoded in the URI.
612      * When we get to a point where we switch to a better globally unique entity identity model,
613      * then a lot of the code being used to calculate an entity url to in-turn generate a
614      * deterministic globally unique id will disappear.
615      * 
616      * 
617      * right now we have the following:
618      * 
619      * - cloud-regions/cloud-region/{cloud-region-id}/{cloud-owner-id}/tenants/tenant/{tenant-id}
620      * 
621      */
622
623     /*
624      * For all entity types use the self-link extraction method to be consistent. Once we have a
625      * globally unique identity mechanism for entities, this logic can be revisited.
626      */
627     ain.clearQueryParams();
628     ain.addQueryParams(extractQueryParamsFromSelfLink(ain.getSelfLink()));
629     ain.changeState(NodeProcessingState.NEIGHBORS_UNPROCESSED,
630         NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_OK);
631
632
633   }
634
635   /**
636    * Perform self link resolve.
637    *
638    * @param nodeId the node id
639    */
640   private void performSelfLinkResolve(String nodeId) {
641
642     if (nodeId == null) {
643       LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR,
644           "Resolve of self-link" + " has been skipped because provided nodeId is null");
645       return;
646     }
647
648     ActiveInventoryNode ain = nodeCache.get(nodeId);
649
650     if (ain == null) {
651       LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Failed to find node with id, " + nodeId
652           + ", from node cache. Resolve self-link method has been skipped.");
653       return;
654     }
655
656     if (!ain.isSelfLinkPendingResolve()) {
657
658       ain.setSelfLinkPendingResolve(true);
659
660       // kick off async self-link resolution
661
662       if (LOG.isDebugEnabled()) {
663         LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
664             "About to process node in SELF_LINK_UNPROCESSED State, link = " + ain.getSelfLink());
665       }
666
667       numLinksDiscovered.incrementAndGet();
668
669       String depthModifier = DEPTH_ALL_MODIFIER;
670
671       /*
672        * If the current node is the search target, we want to see everything the node has to offer
673        * from the self-link and not filter it to a single node.
674        */
675
676       if (shallowEntities.contains(ain.getEntityType()) && !ain.isRootNode()) {
677         depthModifier = NODES_ONLY_MODIFIER;
678       }
679
680       NodeProcessingTransaction txn = new NodeProcessingTransaction();
681       txn.setProcessingNode(ain);
682       txn.setRequestParameters(depthModifier);
683       aaiWorkOnHand.incrementAndGet();
684       supplyAsync(new PerformNodeSelfLinkProcessingTask(txn, depthModifier, aaiAdapter, aaiConfig),
685           aaiExecutorService).whenComplete((nodeTxn, error) -> {
686             aaiWorkOnHand.decrementAndGet();
687             if (error != null) {
688
689               /*
690                * an error processing the self link should probably result in the node processing
691                * state shifting to ERROR
692                */
693
694               nodeTxn.getProcessingNode().setSelflinkRetrievalFailure(true);
695
696               nodeTxn.getProcessingNode().changeState(NodeProcessingState.ERROR,
697                   NodeProcessingAction.SELF_LINK_RESOLVE_ERROR);
698
699               nodeTxn.getProcessingNode().setSelfLinkPendingResolve(false);
700
701             } else {
702
703               totalLinksRetrieved.incrementAndGet();
704
705               OperationResult opResult = nodeTxn.getOpResult();
706
707               if (opResult != null && opResult.wasSuccessful()) {
708
709                 if (!opResult.wasSuccessful()) {
710                   numFailedLinkResolve.incrementAndGet();
711                 }
712
713                 if (opResult.isFromCache()) {
714                   numSuccessfulLinkResolveFromCache.incrementAndGet();
715                 } else {
716                   numSuccessfulLinkResolveFromFromServer.incrementAndGet();
717                 }
718
719                 // success path
720                 nodeTxn.getProcessingNode().setOpResult(opResult);
721                 nodeTxn.getProcessingNode().changeState(
722                     NodeProcessingState.SELF_LINK_RESPONSE_UNPROCESSED,
723                     NodeProcessingAction.SELF_LINK_RESOLVE_OK);
724
725                 nodeTxn.getProcessingNode().setSelfLinkProcessed(true);
726                 nodeTxn.getProcessingNode().setSelfLinkPendingResolve(false);
727
728               } else {
729                 LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR,
730                     "Self Link retrieval for link," + txn.getSelfLinkWithModifiers()
731                         + ", failed with error code," + nodeTxn.getOpResult().getResultCode()
732                         + ", and message," + nodeTxn.getOpResult().getResult());
733
734                 nodeTxn.getProcessingNode().setSelflinkRetrievalFailure(true);
735                 nodeTxn.getProcessingNode().setSelfLinkProcessed(true);
736
737                 nodeTxn.getProcessingNode().changeState(NodeProcessingState.ERROR,
738                     NodeProcessingAction.SELF_LINK_RESOLVE_ERROR);
739
740                 nodeTxn.getProcessingNode().setSelfLinkPendingResolve(false);
741
742               }
743             }
744
745           });
746
747     }
748
749   }
750
751
752   /**
753    * Process neighbors.
754    *
755    * @param nodeId the node id
756    */
757   private void processNeighbors(String nodeId) {
758
759     if (nodeId == null) {
760       LOG.error(AaiUiMsgs.SELF_LINK_PROCESS_NEIGHBORS_ERROR,
761           "Failed to process" + " neighbors because nodeId is null.");
762       return;
763     }
764
765     ActiveInventoryNode ain = nodeCache.get(nodeId);
766
767     if (ain == null) {
768       LOG.error(AaiUiMsgs.SELF_LINK_PROCESS_NEIGHBORS_ERROR, "Failed to process"
769           + " neighbors because node could not be found in nodeCache with id, " + nodeId);
770       return;
771     }
772
773     /*
774      * process complex attribute and relationships
775      */
776
777     boolean neighborsProcessedSuccessfully = true;
778
779     for (JsonNode n : ain.getComplexGroups()) {
780       neighborsProcessedSuccessfully &= decodeComplexAttributeGroup(ain, n);
781     }
782
783     for (RelationshipList relationshipList : ain.getRelationshipLists()) {
784       neighborsProcessedSuccessfully &= addSelfLinkRelationshipChildren(ain, relationshipList);
785     }
786
787
788     if (neighborsProcessedSuccessfully) {
789       ain.changeState(NodeProcessingState.READY, NodeProcessingAction.NEIGHBORS_PROCESSED_OK);
790     } else {
791       ain.changeState(NodeProcessingState.ERROR, NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
792     }
793
794
795     /*
796      * If neighbors fail to process, there is already a call to change the state within the
797      * relationship and neighbor processing functions.
798      */
799
800   }
801
802   /**
803    * Find and mark root node.
804    *
805    * @param queryParams the query params
806    * @return true, if successful
807    */
808   private boolean findAndMarkRootNode(QueryParams queryParams) {
809
810     for (ActiveInventoryNode cacheNode : nodeCache.values()) {
811
812       if (queryParams.getSearchTargetNodeId().equals(cacheNode.getNodeId())) {
813         cacheNode.setNodeDepth(0);
814         cacheNode.setRootNode(true);
815         LOG.info(AaiUiMsgs.ROOT_NODE_DISCOVERED, queryParams.getSearchTargetNodeId());
816         return true;
817       }
818     }
819
820     return false;
821
822   }
823
824   /**
825    * Process current node states.
826    *
827    * @param rootNodeDiscovered the root node discovered
828    */
829   private void processCurrentNodeStates(boolean rootNodeDiscovered) {
830     /*
831      * Force an evaluation of node depths before determining if we should limit state-based
832      * traversal or processing.
833      */
834     if (rootNodeDiscovered) {
835       evaluateNodeDepths();
836     }
837
838     for (ActiveInventoryNode cacheNode : nodeCache.values()) {
839
840       if (LOG.isDebugEnabled()) {
841         LOG.debug(AaiUiMsgs.DEBUG_GENERIC, "processCurrentNodeState(), nid = "
842             + cacheNode.getNodeId() + " , nodeDepth = " + cacheNode.getNodeDepth());
843       }
844
845       switch (cacheNode.getState()) {
846
847         case INIT: {
848           processInitialState(cacheNode.getNodeId());
849           break;
850         }
851
852         case READY:
853         case ERROR: {
854           break;
855         }
856
857         case SELF_LINK_UNRESOLVED: {
858           performSelfLinkResolve(cacheNode.getNodeId());
859           break;
860         }
861
862         case SELF_LINK_RESPONSE_UNPROCESSED: {
863           processSelfLinkResponse(cacheNode.getNodeId());
864           break;
865         }
866
867         case NEIGHBORS_UNPROCESSED: {
868
869           /*
870            * We use the rootNodeDiscovered flag to ignore depth retrieval thresholds until the root
871            * node is identified. Then the evaluative depth calculations should re-balance the graph
872            * around the root node.
873            */
874
875           if (!rootNodeDiscovered || cacheNode.getNodeDepth() < this.visualizationConfigs
876               .getMaxSelfLinkTraversalDepth()) {
877
878             if (LOG.isDebugEnabled()) {
879               LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
880                   "SLNC::processCurrentNodeState() -- Node at max depth,"
881                       + " halting processing at current state = -- " + cacheNode.getState()
882                       + " nodeId = " + cacheNode.getNodeId());
883             }
884
885
886
887             processNeighbors(cacheNode.getNodeId());
888
889           }
890
891           break;
892         }
893         default:
894           break;
895
896
897
898       }
899
900     }
901
902   }
903
904   /**
905    * Adds the complex group to node.
906    *
907    * @param targetNode the target node
908    * @param attributeGroup the attribute group
909    * @return true, if successful
910    */
911   private boolean addComplexGroupToNode(ActiveInventoryNode targetNode, JsonNode attributeGroup) {
912
913     if (attributeGroup == null) {
914       targetNode.changeState(NodeProcessingState.ERROR,
915           NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_OK);
916       return false;
917     }
918
919     RelationshipList relationshipList = null;
920
921     if (attributeGroup.isObject()) {
922
923       Iterator<Entry<String, JsonNode>> fields = attributeGroup.fields();
924       Entry<String, JsonNode> field = null;
925       String fieldName;
926       JsonNode fieldValue;
927
928       while (fields.hasNext()) {
929         field = fields.next();
930         fieldName = field.getKey();
931         fieldValue = field.getValue();
932
933         if (fieldValue.isObject()) {
934
935           if (fieldName.equals("relationship-list")) {
936
937             try {
938               relationshipList =
939                   mapper.readValue(field.getValue().toString(), RelationshipList.class);
940
941               if (relationshipList != null) {
942                 targetNode.addRelationshipList(relationshipList);
943               }
944
945             } catch (Exception exc) {
946               LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR,
947                   "Failed to parse" + " relationship-list attribute. Parse resulted in error, "
948                       + exc.getLocalizedMessage());
949               targetNode.changeState(NodeProcessingState.ERROR,
950                   NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_ERROR);
951               return false;
952             }
953
954           } else {
955             targetNode.addComplexGroup(fieldValue);
956           }
957
958         } else if (fieldValue.isArray()) {
959           if (LOG.isDebugEnabled()) {
960             LOG.debug(AaiUiMsgs.DEBUG_GENERIC, "Unexpected array type with a key = " + fieldName);
961           }
962         } else if (fieldValue.isValueNode()) {
963           if (OxmEntityLookup.getInstance().getEntityDescriptors().get(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" + " keyName, " + entityType
1044             + "." + pkeyNames.get(0) + ", from relationship data, " + r.toString());
1045         return null;
1046       }
1047
1048       for (int i = 1; i < pkeyNames.size(); i++) {
1049
1050         String kv = extractKeyValueFromRelationData(r, entityType + "." + pkeyNames.get(i));
1051         if (kv != null) {
1052           sb.append("/").append(kv);
1053         } else {
1054           // this should be a fatal error because unless we can
1055           // successfully retrieve all the expected keys we'll end up
1056           // with a garbage node
1057           LOG.error(AaiUiMsgs.EXTRACTION_ERROR, "ERROR:  failed to extract keyName, " + entityType
1058               + "." + pkeyNames.get(i) + ", from relationship data, " + r.toString());
1059           return null;
1060         }
1061       }
1062
1063       return sb.toString();
1064
1065     }
1066
1067     return null;
1068
1069   }
1070
1071   /**
1072    * Extract key value from relation data.
1073    *
1074    * @param r the r
1075    * @param keyName the key name
1076    * @return the string
1077    */
1078   private String extractKeyValueFromRelationData(Relationship r, String keyName) {
1079
1080     RelationshipData[] rdList = r.getRelationshipData();
1081
1082     for (RelationshipData relData : rdList) {
1083
1084       if (relData.getRelationshipKey().equals(keyName)) {
1085         return relData.getRelationshipValue();
1086       }
1087     }
1088
1089     return null;
1090   }
1091
1092   /**
1093    * Determine node id and key.
1094    *
1095    * @param ain the ain
1096    * @return true, if successful
1097    */
1098   private boolean addNodeQueryParams(ActiveInventoryNode ain) {
1099
1100     if (ain == null) {
1101       LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE_NODE_ID, "ActiveInventoryNode is null");
1102       return false;
1103     }
1104
1105     List<String> pkeyNames = OxmEntityLookup.getInstance().getEntityDescriptors()
1106         .get(ain.getEntityType()).getPrimaryKeyAttributeNames();
1107
1108     if (pkeyNames == null || pkeyNames.size() == 0) {
1109       LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE_NODE_ID, "Primary key names is empty");
1110       return false;
1111     }
1112
1113     StringBuilder sb = new StringBuilder(64);
1114
1115     if (pkeyNames.size() > 0) {
1116       String primaryKey = ain.getProperties().get(pkeyNames.get(0));
1117       if (primaryKey != null) {
1118         sb.append(primaryKey);
1119       } else {
1120         // this should be a fatal error because unless we can
1121         // successfully retrieve all the expected keys we'll end up
1122         // with a garbage node
1123         LOG.error(AaiUiMsgs.EXTRACTION_ERROR,
1124             "ERROR: Failed to extract keyName, " + pkeyNames.get(0) + ", from entity properties");
1125         return false;
1126       }
1127
1128       for (int i = 1; i < pkeyNames.size(); i++) {
1129
1130         String kv = ain.getProperties().get(pkeyNames.get(i));
1131         if (kv != null) {
1132           sb.append("/").append(kv);
1133         } else {
1134           // this should be a fatal error because unless we can
1135           // successfully retrieve all the expected keys we'll end up
1136           // with a garbage node
1137           LOG.error(AaiUiMsgs.EXTRACTION_ERROR,
1138               "ERROR: Failed to extract keyName, " + pkeyNames.get(i) + ", from entity properties");
1139           return false;
1140         }
1141       }
1142
1143       /*
1144        * final String nodeId = NodeUtils.generateUniqueShaDigest(ain.getEntityType(),
1145        * NodeUtils.concatArray(pkeyNames, "/"), sb.toString());
1146        */
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     Relationship[] relationshipArray = relationshipList.getRelationshipList();
1184     OxmEntityDescriptor descriptor = null;
1185     String repairedSelfLink = 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(this.visualizationConfigs);
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 = OxmEntityLookup.getInstance().getEntityDescriptors().get(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.getPrimaryKeyAttributeNames();
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(this.visualizationConfigs);
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() < this.visualizationConfigs.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 (this.visualizationConfigs.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 =
1630         OxmEntityLookup.getInstance().getEntityDescriptors().get(entityType);
1631
1632     if (descriptor == null) {
1633       LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE,
1634           "oxm entity" + " descriptor for entityType = " + entityType);
1635       return null;
1636     }
1637
1638     List<String> pkeyNames = descriptor.getPrimaryKeyAttributeNames();
1639
1640     if (pkeyNames == null || pkeyNames.size() == 0) {
1641       LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE,
1642           "node primary" + " key because descriptor primary key names is empty");
1643       return null;
1644     }
1645
1646     return NodeUtils.concatArray(pkeyNames, "/");
1647
1648   }
1649
1650 }