summaryrefslogtreecommitdiffstats
path: root/src/main/java/org/openecomp/sparky/viewandinspect/services/VisualizationContext.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/org/openecomp/sparky/viewandinspect/services/VisualizationContext.java')
-rw-r--r--src/main/java/org/openecomp/sparky/viewandinspect/services/VisualizationContext.java1649
1 files changed, 1649 insertions, 0 deletions
diff --git a/src/main/java/org/openecomp/sparky/viewandinspect/services/VisualizationContext.java b/src/main/java/org/openecomp/sparky/viewandinspect/services/VisualizationContext.java
new file mode 100644
index 0000000..c5adfd4
--- /dev/null
+++ b/src/main/java/org/openecomp/sparky/viewandinspect/services/VisualizationContext.java
@@ -0,0 +1,1649 @@
+/**
+ * ============LICENSE_START===================================================
+ * SPARKY (AAI UI service)
+ * ============================================================================
+ * Copyright © 2017 AT&T Intellectual Property.
+ * Copyright © 2017 Amdocs
+ * All rights reserved.
+ * ============================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=====================================================
+ *
+ * ECOMP and OpenECOMP are trademarks
+ * and service marks of AT&T Intellectual Property.
+ */
+
+package org.openecomp.sparky.viewandinspect.services;
+
+import static java.util.concurrent.CompletableFuture.supplyAsync;
+
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.http.client.utils.URIBuilder;
+import org.openecomp.cl.api.Logger;
+import org.openecomp.cl.eelf.LoggerFactory;
+import org.openecomp.sparky.config.oxm.OxmEntityDescriptor;
+import org.openecomp.sparky.config.oxm.OxmModelLoader;
+import org.openecomp.sparky.dal.aai.ActiveInventoryDataProvider;
+import org.openecomp.sparky.dal.aai.config.ActiveInventoryConfig;
+import org.openecomp.sparky.dal.rest.OperationResult;
+import org.openecomp.sparky.logging.AaiUiMsgs;
+import org.openecomp.sparky.synchronizer.entity.SearchableEntity;
+import org.openecomp.sparky.util.NodeUtils;
+import org.openecomp.sparky.viewandinspect.config.TierSupportUiConstants;
+import org.openecomp.sparky.viewandinspect.config.VisualizationConfig;
+import org.openecomp.sparky.viewandinspect.entity.ActiveInventoryNode;
+import org.openecomp.sparky.viewandinspect.entity.InlineMessage;
+import org.openecomp.sparky.viewandinspect.entity.NodeProcessingTransaction;
+import org.openecomp.sparky.viewandinspect.entity.QueryParams;
+import org.openecomp.sparky.viewandinspect.entity.Relationship;
+import org.openecomp.sparky.viewandinspect.entity.RelationshipData;
+import org.openecomp.sparky.viewandinspect.entity.RelationshipList;
+import org.openecomp.sparky.viewandinspect.entity.SelfLinkDeterminationTransaction;
+import org.openecomp.sparky.viewandinspect.enumeration.NodeProcessingAction;
+import org.openecomp.sparky.viewandinspect.enumeration.NodeProcessingState;
+import org.openecomp.sparky.viewandinspect.task.PerformNodeSelfLinkProcessingTask;
+import org.openecomp.sparky.viewandinspect.task.PerformSelfLinkDeterminationTask;
+
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+
+/**
+ * The Class SelfLinkNodeCollector.
+ */
+public class VisualizationContext {
+
+ private static final int MAX_DEPTH_EVALUATION_ATTEMPTS = 100;
+ private static final String DEPTH_ALL_MODIFIER = "?depth=all";
+ private static final String NODES_ONLY_MODIFIER = "?nodes-only";
+ private static final String SERVICE_INSTANCE = "service-instance";
+
+ private static final Logger LOG = LoggerFactory.getInstance().getLogger(
+ VisualizationContext.class);
+ private final ActiveInventoryDataProvider aaiProvider;
+
+ private int maxSelfLinkTraversalDepth;
+ private AtomicInteger numLinksDiscovered;
+ private AtomicInteger numSuccessfulLinkResolveFromCache;
+ private AtomicInteger numSuccessfulLinkResolveFromFromServer;
+ private AtomicInteger numFailedLinkResolve;
+ private AtomicInteger aaiWorkOnHand;
+
+ private ActiveInventoryConfig aaiConfig;
+ private VisualizationConfig visualizationConfig;
+ private List<String> shallowEntities;
+
+ private AtomicInteger totalLinksRetrieved;
+
+ private final long contextId;
+ private final String contextIdStr;
+
+ private OxmModelLoader loader;
+ private ObjectMapper mapper;
+ private InlineMessage inlineMessage = null;
+
+ private ExecutorService aaiExecutorService;
+
+ /*
+ * The node cache is intended to be a flat structure indexed by a primary key to avoid needlessly
+ * re-requesting the same self-links over-and-over again, to speed up the overall render time and
+ * more importantly to reduce the network cost of determining information we already have.
+ */
+ private ConcurrentHashMap<String, ActiveInventoryNode> nodeCache;
+
+ /**
+ * Instantiates a new self link node collector.
+ *
+ * @param loader the loader
+ * @throws Exception the exception
+ */
+ public VisualizationContext(long contextId, ActiveInventoryDataProvider aaiDataProvider,
+ ExecutorService aaiExecutorService, OxmModelLoader loader) throws Exception {
+
+ this.contextId = contextId;
+ this.contextIdStr = "[Context-Id=" + contextId + "]";
+ this.aaiProvider = aaiDataProvider;
+ this.aaiExecutorService = aaiExecutorService;
+ this.loader = loader;
+
+ this.nodeCache = new ConcurrentHashMap<String, ActiveInventoryNode>();
+ this.numLinksDiscovered = new AtomicInteger(0);
+ this.totalLinksRetrieved = new AtomicInteger(0);
+ this.numSuccessfulLinkResolveFromCache = new AtomicInteger(0);
+ this.numSuccessfulLinkResolveFromFromServer = new AtomicInteger(0);
+ this.numFailedLinkResolve = new AtomicInteger(0);
+ this.aaiWorkOnHand = new AtomicInteger(0);
+
+ this.aaiConfig = ActiveInventoryConfig.getConfig();
+ this.visualizationConfig = VisualizationConfig.getConfig();
+ this.shallowEntities = aaiConfig.getAaiRestConfig().getShallowEntities();
+
+ this.maxSelfLinkTraversalDepth = visualizationConfig.getMaxSelfLinkTraversalDepth();
+
+ this.mapper = new ObjectMapper();
+ mapper.setSerializationInclusion(Include.NON_EMPTY);
+ mapper.setPropertyNamingStrategy(new PropertyNamingStrategy.KebabCaseStrategy());
+ }
+
+ public long getContextId() {
+ return contextId;
+ }
+
+ /**
+ * A utility method for extracting all entity-type primary key values from a provided self-link
+ * and return a set of generic-query API keys.
+ *
+ * @param parentEntityType
+ * @param link
+ * @return a list of key values that can be used for this entity with the AAI generic-query API
+ */
+ protected List<String> extractQueryParamsFromSelfLink(String link) {
+
+ List<String> queryParams = new ArrayList<String>();
+
+ if (link == null) {
+ LOG.error(AaiUiMsgs.QUERY_PARAM_EXTRACTION_ERROR, "self link is null");
+ return queryParams;
+ }
+
+ Map<String, OxmEntityDescriptor> entityDescriptors = loader.getEntityDescriptors();
+
+ try {
+
+ URIBuilder urlBuilder = new URIBuilder(link);
+ String urlPath = urlBuilder.getPath();
+
+ OxmEntityDescriptor descriptor = null;
+ String[] urlPathElements = urlPath.split("/");
+ List<String> primaryKeyNames = null;
+ int index = 0;
+ String entityType = null;
+
+ while (index < urlPathElements.length) {
+
+ descriptor = entityDescriptors.get(urlPathElements[index]);
+
+ if (descriptor != null) {
+ entityType = urlPathElements[index];
+ primaryKeyNames = descriptor.getPrimaryKeyAttributeName();
+
+ /*
+ * Make sure from what ever index we matched the parent entity-type on that we can extract
+ * additional path elements for the primary key values.
+ */
+
+ if (index + primaryKeyNames.size() < urlPathElements.length) {
+
+ for (String primaryKeyName : primaryKeyNames) {
+ index++;
+ queryParams.add(entityType + "." + primaryKeyName + ":" + urlPathElements[index]);
+ }
+ } else {
+ LOG.error(AaiUiMsgs.QUERY_PARAM_EXTRACTION_ERROR,
+ "Could not extract query parametrs for entity-type = '" + entityType
+ + "' from self-link = " + link);
+ }
+ }
+
+ index++;
+ }
+
+ } catch (URISyntaxException exc) {
+
+ LOG.error(AaiUiMsgs.QUERY_PARAM_EXTRACTION_ERROR,
+ "Error extracting query parameters from self-link = " + link + ". Error = "
+ + exc.getMessage());
+ }
+
+ return queryParams;
+
+ }
+
+ /**
+ * Decode complex attribute group.
+ *
+ * @param ain the ain
+ * @param attributeGroup the attribute group
+ * @return boolean indicating whether operation was successful (true), / failure(false).
+ */
+ public boolean decodeComplexAttributeGroup(ActiveInventoryNode ain, JsonNode attributeGroup) {
+
+ try {
+
+ Iterator<Entry<String, JsonNode>> entityArrays = attributeGroup.fields();
+ Entry<String, JsonNode> entityArray = null;
+
+ if (entityArrays == null) {
+ LOG.error(AaiUiMsgs.ATTRIBUTE_GROUP_FAILURE, attributeGroup.toString());
+ ain.changeState(NodeProcessingState.ERROR, NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
+ return false;
+ }
+
+ while (entityArrays.hasNext()) {
+
+ entityArray = entityArrays.next();
+
+ String entityType = entityArray.getKey();
+ JsonNode entityArrayObject = entityArray.getValue();
+
+ if (entityArrayObject.isArray()) {
+
+ Iterator<JsonNode> entityCollection = entityArrayObject.elements();
+ JsonNode entity = null;
+ while (entityCollection.hasNext()) {
+ entity = entityCollection.next();
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(AaiUiMsgs.DEBUG_GENERIC, "decodeComplexAttributeGroup(),"
+ + " entity = " + entity.toString());
+ }
+
+ /**
+ * Here's what we are going to do:
+ *
+ * <li>In the ActiveInventoryNode, on construction maintain a collection of queryParams
+ * that is added to for the purpose of discovering parent->child hierarchies.
+ *
+ * <li>When we hit this block of the code then we'll use the queryParams to feed the
+ * generic query to resolve the self-link asynchronously.
+ *
+ * <li>Upon successful link determination, then and only then will we create a new node
+ * in the nodeCache and process the child
+ *
+ */
+
+ ActiveInventoryNode newNode = new ActiveInventoryNode();
+ newNode.setEntityType(entityType);
+
+ /*
+ * This is partially a lie because we actually don't have a self-link for complex nodes
+ * discovered in this way.
+ */
+ newNode.setSelfLinkProcessed(true);
+ newNode.changeState(NodeProcessingState.SELF_LINK_RESPONSE_UNPROCESSED,
+ NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_OK);
+
+ /*
+ * copy parent query params into new child
+ */
+
+ if (SERVICE_INSTANCE.equals(entityType)) {
+
+ /*
+ * 1707 AAI has an issue being tracked with AAI-8932 where the generic-query cannot be
+ * resolved if all the service-instance path keys are provided. The query only works
+ * if only the service-instance key and valude are passed due to a historical reason.
+ * A fix is being worked on for 1707, and when it becomes available we can revert this
+ * small change.
+ */
+
+ newNode.clearQueryParams();
+
+ } else {
+
+ /*
+ * For all other entity-types we want to copy the parent query parameters into the new node
+ * query parameters.
+ */
+
+ for (String queryParam : ain.getQueryParams()) {
+ newNode.addQueryParam(queryParam);
+ }
+
+ }
+
+
+ if (!addComplexGroupToNode(newNode, entity)) {
+ LOG.error(AaiUiMsgs.ATTRIBUTE_GROUP_FAILURE, "Failed to add child to parent for child = " + entity.toString());
+ }
+
+ if (!addNodeQueryParams(newNode)) {
+ LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE_NODE_ID, "Error determining node id and key for node = " + newNode.dumpNodeTree(true)
+ + " skipping relationship processing");
+ newNode.changeState(NodeProcessingState.ERROR,
+ NodeProcessingAction.NODE_IDENTITY_ERROR);
+ return false;
+ } else {
+
+ newNode.changeState(NodeProcessingState.NEIGHBORS_UNPROCESSED,
+ NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_OK);
+
+ }
+
+
+ /*
+ * Order matters for the query params. We need to set the parent ones before the child
+ * node
+ */
+
+ String selfLinkQuery =
+ aaiProvider.getGenericQueryForSelfLink(entityType, newNode.getQueryParams());
+
+ /**
+ * <li>get the self-link
+ * <li>add it to the new node
+ * <li>generate node id
+ * <li>add node to node cache
+ * <li>add node id to parent outbound links list
+ * <li>process node children (should be automatic) (but don't query and resolve
+ * self-link as we already have all the data)
+ */
+
+ SelfLinkDeterminationTransaction txn = new SelfLinkDeterminationTransaction();
+
+ txn.setQueryString(selfLinkQuery);
+ txn.setNewNode(newNode);
+ txn.setParentNodeId(ain.getNodeId());
+ aaiWorkOnHand.incrementAndGet();
+ supplyAsync(new PerformSelfLinkDeterminationTask(txn, null, aaiProvider),
+ aaiExecutorService).whenComplete((nodeTxn, error) -> {
+ aaiWorkOnHand.decrementAndGet();
+ if (error != null) {
+ LOG.error(AaiUiMsgs.SELF_LINK_DETERMINATION_FAILED_GENERIC, selfLinkQuery);
+ } else {
+
+ OperationResult opResult = nodeTxn.getOpResult();
+
+ ActiveInventoryNode newChildNode = txn.getNewNode();
+
+ if (opResult != null && opResult.wasSuccessful()) {
+
+ if (opResult.isResolvedLinkFailure()) {
+ numFailedLinkResolve.incrementAndGet();
+ }
+
+ if (opResult.isResolvedLinkFromCache()) {
+ numSuccessfulLinkResolveFromCache.incrementAndGet();
+ }
+
+ if (opResult.isResolvedLinkFromServer()) {
+ numSuccessfulLinkResolveFromFromServer.incrementAndGet();
+ }
+
+ /*
+ * extract the self-link from the operational result.
+ */
+
+ Collection<JsonNode> entityLinks = new ArrayList<JsonNode>();
+ JsonNode genericQueryResult = null;
+ try {
+ genericQueryResult =
+ NodeUtils.convertJsonStrToJsonNode(nodeTxn.getOpResult().getResult());
+ } catch (Exception exc) {
+ LOG.error(AaiUiMsgs.JSON_CONVERSION_ERROR, JsonNode.class.toString(), exc.getMessage());
+ }
+
+ NodeUtils.extractObjectsByKey(genericQueryResult, "resource-link",
+ entityLinks);
+
+ String selfLink = null;
+
+ if (entityLinks.size() != 1) {
+
+ LOG.error(AaiUiMsgs.SELF_LINK_DETERMINATION_FAILED_UNEXPECTED_LINKS, String.valueOf(entityLinks.size()));
+
+ } else {
+ selfLink = ((JsonNode) entityLinks.toArray()[0]).asText();
+
+ newChildNode.setSelfLink(selfLink);
+ newChildNode.setNodeId(NodeUtils.generateUniqueShaDigest(selfLink));
+
+ String uri = NodeUtils.calculateEditAttributeUri(selfLink);
+ if (uri != null) {
+ newChildNode.addProperty(TierSupportUiConstants.URI_ATTR_NAME, uri);
+ }
+
+ ActiveInventoryNode parent = nodeCache.get(txn.getParentNodeId());
+
+ if (parent != null) {
+ parent.addOutboundNeighbor(newChildNode.getNodeId());
+ newChildNode.addInboundNeighbor(parent.getNodeId());
+ }
+
+ newChildNode.setSelfLinkPendingResolve(false);
+ newChildNode.setSelfLinkProcessed(true);
+
+ newChildNode.changeState(NodeProcessingState.NEIGHBORS_UNPROCESSED,
+ NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_OK);
+
+ nodeCache.putIfAbsent(newChildNode.getNodeId(), newChildNode);
+
+ }
+
+ } else {
+ LOG.error(AaiUiMsgs.SELF_LINK_RETRIEVAL_FAILED, txn.getQueryString(),
+ String.valueOf(nodeTxn.getOpResult().getResultCode()), nodeTxn.getOpResult().getResult());
+ newChildNode.setSelflinkRetrievalFailure(true);
+ newChildNode.setSelfLinkProcessed(true);
+ newChildNode.setSelfLinkPendingResolve(false);
+
+ newChildNode.changeState(NodeProcessingState.ERROR,
+ NodeProcessingAction.SELF_LINK_DETERMINATION_ERROR);
+
+ }
+
+ }
+
+ });
+
+ }
+
+ return true;
+
+ } else {
+ LOG.error(AaiUiMsgs.UNHANDLED_OBJ_TYPE_FOR_ENTITY_TYPE, entityType);
+ }
+
+ }
+ } catch (Exception exc) {
+ LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Exception caught while"
+ + " decoding complex attribute group - " + exc.getMessage());
+ }
+
+ return false;
+
+ }
+
+ /**
+ * Process self link response.
+ *
+ * @param nodeId the node id
+ */
+ private void processSelfLinkResponse(String nodeId) {
+
+ if (nodeId == null) {
+ LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Cannot process self link"
+ + " response because nodeId is null");
+ return;
+ }
+
+ ActiveInventoryNode ain = nodeCache.get(nodeId);
+
+ if (ain == null) {
+ LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Cannot process self link response"
+ + " because can't find node for id = " + nodeId);
+ return;
+ }
+
+ JsonNode jsonNode = null;
+
+ try {
+ jsonNode = mapper.readValue(ain.getOpResult().getResult(), JsonNode.class);
+ } catch (Exception exc) {
+ LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to marshal json"
+ + " response str into JsonNode with error, " + exc.getLocalizedMessage());
+ ain.changeState(NodeProcessingState.ERROR,
+ NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_ERROR);
+ return;
+ }
+
+ if (jsonNode == null) {
+ LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to parse json node str."
+ + " Parse resulted a null value.");
+ ain.changeState(NodeProcessingState.ERROR,
+ NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_ERROR);
+ return;
+ }
+
+ Iterator<Entry<String, JsonNode>> fieldNames = jsonNode.fields();
+ Entry<String, JsonNode> field = null;
+
+ RelationshipList relationshipList = null;
+
+ while (fieldNames.hasNext()) {
+
+ field = fieldNames.next();
+ String fieldName = field.getKey();
+
+ if ("relationship-list".equals(fieldName)) {
+
+ try {
+ relationshipList = mapper.readValue(field.getValue().toString(), RelationshipList.class);
+
+ if (relationshipList != null) {
+ ain.addRelationshipList(relationshipList);
+ }
+
+ } catch (Exception exc) {
+ LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to parse relationship-list"
+ + " attribute. Parse resulted in error, " + exc.getLocalizedMessage());
+ ain.changeState(NodeProcessingState.ERROR,
+ NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_ERROR);
+ return;
+ }
+
+ } else {
+
+ JsonNode nodeValue = field.getValue();
+
+ if (nodeValue != null && nodeValue.isValueNode()) {
+
+ if (loader.getEntityDescriptor(fieldName) == null) {
+
+ /*
+ * entity property name is not an entity, thus we can add this property name and value
+ * to our property set
+ */
+
+ ain.addProperty(fieldName, nodeValue.asText());
+
+ }
+
+ } else {
+
+ if (nodeValue.isArray()) {
+
+ if (loader.getEntityDescriptor(fieldName) == null) {
+
+ /*
+ * entity property name is not an entity, thus we can add this property name and value
+ * to our property set
+ */
+
+ ain.addProperty(field.getKey(), nodeValue.toString());
+
+ }
+
+ } else {
+
+ ain.addComplexGroup(nodeValue);
+
+ }
+
+ }
+ }
+
+ }
+
+ String uri = NodeUtils.calculateEditAttributeUri(ain.getSelfLink());
+ if (uri != null) {
+ ain.addProperty(TierSupportUiConstants.URI_ATTR_NAME, uri);
+ }
+
+ /*
+ * We need a special behavior for intermediate entities from the REST model
+ *
+ * Tenants are not top level entities, and when we want to visualization
+ * their children, we need to construct keys that include the parent entity query
+ * keys, the current entity type keys, and the child keys. We'll always have the
+ * current entity and children, but never the parent entity in the current (1707) REST
+ * data model.
+ *
+ * We have two possible solutions:
+ *
+ * 1) Try to use the custom-query approach to learn about the entity keys
+ * - this could be done, but it could be very expensive for large objects. When we do the first
+ * query to get a tenant, it will list all the in and out edges related to this entity,
+ * there is presently no way to filter this. But the approach could be made to work and it would be
+ * somewhat data-model driven, other than the fact that we have to first realize that the entity
+ * that is being searched for is not top-level entity. Once we have globally unique ids for resources
+ * this logic will not be needed and everything will be simpler. The only reason we are in this logic
+ * at all is to be able to calculate a url for the child entities so we can hash it to generate
+ * a globally unique id that can be safely used for the node.
+ *
+ * *2* Extract the keys from the pathed self-link.
+ * This is a bad solution and I don't like it but it will be fast for all resource types, as the
+ * information is already encoded in the URI. When we get to a point where we switch to a better
+ * globally unique entity identity model, then a lot of the code being used to calculate an entity url
+ * to in-turn generate a deterministic globally unique id will disappear.
+ *
+ *
+ * right now we have the following:
+ *
+ * - cloud-regions/cloud-region/{cloud-region-id}/{cloud-owner-id}/tenants/tenant/{tenant-id}
+ *
+ */
+
+ /*
+ * For all entity types use the self-link extraction method to be consistent. Once we have a
+ * globally unique identity mechanism for entities, this logic can be revisited.
+ */
+ ain.clearQueryParams();
+ ain.addQueryParams(extractQueryParamsFromSelfLink(ain.getSelfLink()));
+
+ ain.changeState(NodeProcessingState.NEIGHBORS_UNPROCESSED,
+ NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_OK);
+
+ }
+
+ /**
+ * Perform self link resolve.
+ *
+ * @param nodeId the node id
+ */
+ private void performSelfLinkResolve(String nodeId) {
+
+ if (nodeId == null) {
+ LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Resolve of self-link"
+ + " has been skipped because provided nodeId is null");
+ return;
+ }
+
+ ActiveInventoryNode ain = nodeCache.get(nodeId);
+
+ if (ain == null) {
+ LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Failed to find node with id, " + nodeId
+ + ", from node cache. Resolve self-link method has been skipped.");
+ return;
+ }
+
+ if (!ain.isSelfLinkPendingResolve()) {
+
+ ain.setSelfLinkPendingResolve(true);
+
+ // kick off async self-link resolution
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
+ "About to process node in SELF_LINK_UNPROCESSED State, link = " + ain.getSelfLink());
+ }
+
+ numLinksDiscovered.incrementAndGet();
+
+ String depthModifier = DEPTH_ALL_MODIFIER;
+
+ /*
+ * If the current node is the search target, we want to see everything the node has to offer
+ * from the self-link and not filter it to a single node.
+ */
+
+ if (shallowEntities.contains(ain.getEntityType()) && !ain.isRootNode()) {
+ depthModifier = NODES_ONLY_MODIFIER;
+ }
+
+ NodeProcessingTransaction txn = new NodeProcessingTransaction();
+ txn.setProcessingNode(ain);
+ txn.setRequestParameters(depthModifier);
+ aaiWorkOnHand.incrementAndGet();
+ supplyAsync(
+ new PerformNodeSelfLinkProcessingTask(txn, depthModifier, aaiProvider),
+ aaiExecutorService).whenComplete((nodeTxn, error) -> {
+ aaiWorkOnHand.decrementAndGet();
+ if (error != null) {
+
+ /*
+ * an error processing the self link should probably result in the node processing
+ * state shifting to ERROR
+ */
+
+ nodeTxn.getProcessingNode().setSelflinkRetrievalFailure(true);
+
+ nodeTxn.getProcessingNode().changeState(NodeProcessingState.ERROR,
+ NodeProcessingAction.SELF_LINK_RESOLVE_ERROR);
+
+ nodeTxn.getProcessingNode().setSelfLinkPendingResolve(false);
+
+ } else {
+
+ totalLinksRetrieved.incrementAndGet();
+
+ OperationResult opResult = nodeTxn.getOpResult();
+
+ if (opResult != null && opResult.wasSuccessful()) {
+
+ if (opResult.isResolvedLinkFailure()) {
+ numFailedLinkResolve.incrementAndGet();
+ }
+
+ if (opResult.isResolvedLinkFromCache()) {
+ numSuccessfulLinkResolveFromCache.incrementAndGet();
+ }
+
+ if (opResult.isResolvedLinkFromServer()) {
+ numSuccessfulLinkResolveFromFromServer.incrementAndGet();
+ }
+
+ // success path
+ nodeTxn.getProcessingNode().setOpResult(opResult);
+ nodeTxn.getProcessingNode().changeState(
+ NodeProcessingState.SELF_LINK_RESPONSE_UNPROCESSED,
+ NodeProcessingAction.SELF_LINK_RESOLVE_OK);
+
+ nodeTxn.getProcessingNode().setSelfLinkProcessed(true);
+ nodeTxn.getProcessingNode().setSelfLinkPendingResolve(false);
+
+ } else {
+ LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Self Link retrieval for link,"
+ + txn.getSelfLinkWithModifiers() + ", failed with error code,"
+ + nodeTxn.getOpResult().getResultCode() + ", and message,"
+ + nodeTxn.getOpResult().getResult());
+
+ nodeTxn.getProcessingNode().setSelflinkRetrievalFailure(true);
+ nodeTxn.getProcessingNode().setSelfLinkProcessed(true);
+
+ nodeTxn.getProcessingNode().changeState(NodeProcessingState.ERROR,
+ NodeProcessingAction.SELF_LINK_RESOLVE_ERROR);
+
+ nodeTxn.getProcessingNode().setSelfLinkPendingResolve(false);
+
+ }
+ }
+
+ });
+
+ }
+
+ }
+
+
+ /**
+ * Process neighbors.
+ *
+ * @param nodeId the node id
+ */
+ private void processNeighbors(String nodeId) {
+
+ if (nodeId == null) {
+ LOG.error(AaiUiMsgs.SELF_LINK_PROCESS_NEIGHBORS_ERROR, "Failed to process"
+ + " neighbors because nodeId is null.");
+ return;
+ }
+
+ ActiveInventoryNode ain = nodeCache.get(nodeId);
+
+ if (ain == null) {
+ LOG.error(AaiUiMsgs.SELF_LINK_PROCESS_NEIGHBORS_ERROR, "Failed to process"
+ + " neighbors because node could not be found in nodeCache with id, " + nodeId);
+ return;
+ }
+
+ /*
+ * process complex attribute and relationships
+ */
+
+ boolean neighborsProcessedSuccessfully = true;
+
+ for (JsonNode n : ain.getComplexGroups()) {
+ neighborsProcessedSuccessfully &= decodeComplexAttributeGroup(ain, n);
+ }
+
+ for (RelationshipList relationshipList : ain.getRelationshipLists()) {
+ neighborsProcessedSuccessfully &= addSelfLinkRelationshipChildren(ain, relationshipList);
+ }
+
+
+ if (neighborsProcessedSuccessfully) {
+ ain.changeState(NodeProcessingState.READY, NodeProcessingAction.NEIGHBORS_PROCESSED_OK);
+ } else {
+ ain.changeState(NodeProcessingState.ERROR, NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
+ }
+
+
+ /*
+ * If neighbors fail to process, there is already a call to change the state within the
+ * relationship and neighbor processing functions.
+ */
+
+ }
+
+ /**
+ * Find and mark root node.
+ *
+ * @param queryParams the query params
+ * @return true, if successful
+ */
+ private boolean findAndMarkRootNode(QueryParams queryParams) {
+
+ for (ActiveInventoryNode cacheNode : nodeCache.values()) {
+
+ if (queryParams.getSearchTargetNodeId().equals(cacheNode.getNodeId())) {
+ cacheNode.setNodeDepth(0);
+ cacheNode.setRootNode(true);
+ LOG.info(AaiUiMsgs.ROOT_NODE_DISCOVERED, queryParams.getSearchTargetNodeId());
+ return true;
+ }
+ }
+
+ return false;
+
+ }
+
+ /**
+ * Process current node states.
+ *
+ * @param rootNodeDiscovered the root node discovered
+ */
+ private void processCurrentNodeStates(boolean rootNodeDiscovered) {
+ /*
+ * Force an evaluation of node depths before determining if we should limit state-based
+ * traversal or processing.
+ */
+ if (rootNodeDiscovered) {
+ evaluateNodeDepths();
+ }
+
+ for (ActiveInventoryNode cacheNode : nodeCache.values()) {
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
+ "processCurrentNodeState(), nid = "
+ + cacheNode.getNodeId() + " , nodeDepth = " + cacheNode.getNodeDepth());
+ }
+
+ switch (cacheNode.getState()) {
+
+ case INIT: {
+ processInitialState(cacheNode.getNodeId());
+ break;
+ }
+
+ case READY:
+ case ERROR: {
+ break;
+ }
+
+ case SELF_LINK_UNRESOLVED: {
+ performSelfLinkResolve(cacheNode.getNodeId());
+ break;
+ }
+
+ case SELF_LINK_RESPONSE_UNPROCESSED: {
+ processSelfLinkResponse(cacheNode.getNodeId());
+ break;
+ }
+
+ case NEIGHBORS_UNPROCESSED: {
+
+ /*
+ * We use the rootNodeDiscovered flag to ignore depth retrieval thresholds until the root
+ * node is identified. Then the evaluative depth calculations should re-balance the graph
+ * around the root node.
+ */
+
+ if (!rootNodeDiscovered || cacheNode.getNodeDepth() < VisualizationConfig.getConfig()
+ .getMaxSelfLinkTraversalDepth()) {
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
+ "SLNC::processCurrentNodeState() -- Node at max depth,"
+ + " halting processing at current state = -- "
+ + cacheNode.getState() + " nodeId = " + cacheNode.getNodeId());
+ }
+
+
+
+ processNeighbors(cacheNode.getNodeId());
+
+ }
+
+ break;
+ }
+ default:
+ break;
+
+
+
+ }
+
+ }
+
+ }
+
+ /**
+ * Adds the complex group to node.
+ *
+ * @param targetNode the target node
+ * @param attributeGroup the attribute group
+ * @return true, if successful
+ */
+ private boolean addComplexGroupToNode(ActiveInventoryNode targetNode, JsonNode attributeGroup) {
+
+ if (attributeGroup == null) {
+ targetNode.changeState(NodeProcessingState.ERROR,
+ NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_OK);
+ return false;
+ }
+
+ RelationshipList relationshipList = null;
+
+ if (attributeGroup.isObject()) {
+
+ Iterator<Entry<String, JsonNode>> fields = attributeGroup.fields();
+ Entry<String, JsonNode> field = null;
+ String fieldName;
+ JsonNode fieldValue;
+
+ while (fields.hasNext()) {
+ field = fields.next();
+ fieldName = field.getKey();
+ fieldValue = field.getValue();
+
+ if (fieldValue.isObject()) {
+
+ if (fieldName.equals("relationship-list")) {
+
+ try {
+ relationshipList =
+ mapper.readValue(field.getValue().toString(), RelationshipList.class);
+
+ if (relationshipList != null) {
+ targetNode.addRelationshipList(relationshipList);
+ }
+
+ } catch (Exception exc) {
+ LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to parse"
+ + " relationship-list attribute. Parse resulted in error, "
+ + exc.getLocalizedMessage());
+ targetNode.changeState(NodeProcessingState.ERROR,
+ NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_ERROR);
+ return false;
+ }
+
+ } else {
+ targetNode.addComplexGroup(fieldValue);
+ }
+
+ } else if (fieldValue.isArray()) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
+ "Unexpected array type with a key = " + fieldName);
+ }
+ } else if (fieldValue.isValueNode()) {
+ if (loader.getEntityDescriptor(field.getKey()) == null) {
+ /*
+ * property key is not an entity type, add it to our property set.
+ */
+ targetNode.addProperty(field.getKey(), fieldValue.asText());
+ }
+
+ }
+ }
+
+ } else if (attributeGroup.isArray()) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
+ "Unexpected array type for attributeGroup = " + attributeGroup);
+ }
+ } else if (attributeGroup.isValueNode()) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
+ "Unexpected value type for attributeGroup = " + attributeGroup);
+ }
+ }
+
+ return true;
+ }
+
+ public int getNumSuccessfulLinkResolveFromCache() {
+ return numSuccessfulLinkResolveFromCache.get();
+ }
+
+ public int getNumSuccessfulLinkResolveFromFromServer() {
+ return numSuccessfulLinkResolveFromFromServer.get();
+ }
+
+ public int getNumFailedLinkResolve() {
+ return numFailedLinkResolve.get();
+ }
+
+ public InlineMessage getInlineMessage() {
+ return inlineMessage;
+ }
+
+ public void setInlineMessage(InlineMessage inlineMessage) {
+ this.inlineMessage = inlineMessage;
+ }
+
+ public void setMaxSelfLinkTraversalDepth(int depth) {
+ this.maxSelfLinkTraversalDepth = depth;
+ }
+
+ public int getMaxSelfLinkTraversalDepth() {
+ return this.maxSelfLinkTraversalDepth;
+ }
+
+ public ConcurrentHashMap<String, ActiveInventoryNode> getNodeCache() {
+ return nodeCache;
+ }
+
+ /**
+ * Gets the relationship primary key values.
+ *
+ * @param r the r
+ * @param entityType the entity type
+ * @param pkeyNames the pkey names
+ * @return the relationship primary key values
+ */
+ private String getRelationshipPrimaryKeyValues(Relationship r, String entityType,
+ List<String> pkeyNames) {
+
+ StringBuilder sb = new StringBuilder(64);
+
+ if (pkeyNames.size() > 0) {
+ String primaryKey = extractKeyValueFromRelationData(r, entityType + "." + pkeyNames.get(0));
+ if (primaryKey != null) {
+
+ sb.append(primaryKey);
+
+ } else {
+ // this should be a fatal error because unless we can
+ // successfully retrieve all the expected keys we'll end up
+ // with a garbage node
+ LOG.error(AaiUiMsgs.EXTRACTION_ERROR, "ERROR: Failed to extract"
+ + " keyName, " + entityType + "." + pkeyNames.get(0)
+ + ", from relationship data, " + r.toString());
+ return null;
+ }
+
+ for (int i = 1; i < pkeyNames.size(); i++) {
+
+ String kv = extractKeyValueFromRelationData(r, entityType + "." + pkeyNames.get(i));
+ if (kv != null) {
+ sb.append("/").append(kv);
+ } else {
+ // this should be a fatal error because unless we can
+ // successfully retrieve all the expected keys we'll end up
+ // with a garbage node
+ LOG.error(AaiUiMsgs.EXTRACTION_ERROR, "ERROR: failed to extract keyName, "
+ + entityType + "." + pkeyNames.get(i)
+ + ", from relationship data, " + r.toString());
+ return null;
+ }
+ }
+
+ return sb.toString();
+
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Extract key value from relation data.
+ *
+ * @param r the r
+ * @param keyName the key name
+ * @return the string
+ */
+ private String extractKeyValueFromRelationData(Relationship r, String keyName) {
+
+ RelationshipData[] rdList = r.getRelationshipData();
+
+ for (RelationshipData relData : rdList) {
+
+ if (relData.getRelationshipKey().equals(keyName)) {
+ return relData.getRelationshipValue();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Determine node id and key.
+ *
+ * @param ain the ain
+ * @return true, if successful
+ */
+ private boolean addNodeQueryParams(ActiveInventoryNode ain) {
+
+ if (ain == null) {
+ LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE_NODE_ID, "ActiveInventoryNode is null");
+ return false;
+ }
+
+ List<String> pkeyNames =
+ loader.getEntityDescriptor(ain.getEntityType()).getPrimaryKeyAttributeName();
+
+ if (pkeyNames == null || pkeyNames.size() == 0) {
+ LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE_NODE_ID, "Primary key names is empty");
+ return false;
+ }
+
+ StringBuilder sb = new StringBuilder(64);
+
+ if (pkeyNames.size() > 0) {
+ String primaryKey = ain.getProperties().get(pkeyNames.get(0));
+ if (primaryKey != null) {
+ sb.append(primaryKey);
+ } else {
+ // this should be a fatal error because unless we can
+ // successfully retrieve all the expected keys we'll end up
+ // with a garbage node
+ LOG.error(AaiUiMsgs.EXTRACTION_ERROR, "ERROR: Failed to extract keyName, "
+ + pkeyNames.get(0) + ", from entity properties");
+ return false;
+ }
+
+ for (int i = 1; i < pkeyNames.size(); i++) {
+
+ String kv = ain.getProperties().get(pkeyNames.get(i));
+ if (kv != null) {
+ sb.append("/").append(kv);
+ } else {
+ // this should be a fatal error because unless we can
+ // successfully retrieve all the expected keys we'll end up
+ // with a garbage node
+ LOG.error(AaiUiMsgs.EXTRACTION_ERROR, "ERROR: Failed to extract keyName, "
+ + pkeyNames.get(i) + ", from entity properties");
+ return false;
+ }
+ }
+
+ /*final String nodeId = NodeUtils.generateUniqueShaDigest(ain.getEntityType(),
+ NodeUtils.concatArray(pkeyNames, "/"), sb.toString());*/
+
+ //ain.setNodeId(nodeId);
+ ain.setPrimaryKeyName(NodeUtils.concatArray(pkeyNames, "/"));
+ ain.setPrimaryKeyValue(sb.toString());
+
+ if (ain.getEntityType() != null && ain.getPrimaryKeyName() != null
+ && ain.getPrimaryKeyValue() != null) {
+ ain.addQueryParam(
+ ain.getEntityType() + "." + ain.getPrimaryKeyName() + ":" + ain.getPrimaryKeyValue());
+ }
+ return true;
+
+ }
+
+ return false;
+
+ }
+
+ /**
+ * Adds the self link relationship children.
+ *
+ * @param processingNode the processing node
+ * @param relationshipList the relationship list
+ * @return true, if successful
+ */
+ private boolean addSelfLinkRelationshipChildren(ActiveInventoryNode processingNode,
+ RelationshipList relationshipList) {
+
+ if (relationshipList == null) {
+ LOG.debug(AaiUiMsgs.DEBUG_GENERIC, "No relationships added to parent node = "
+ + processingNode.getNodeId() + " because relationshipList is empty");
+ processingNode.changeState(NodeProcessingState.ERROR,
+ NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
+ return false;
+ }
+
+ OxmModelLoader modelLoader = OxmModelLoader.getInstance();
+
+ Relationship[] relationshipArray = relationshipList.getRelationshipList();
+ OxmEntityDescriptor descriptor = null;
+ String repairedSelfLink = null;
+
+ if (relationshipArray != null) {
+
+ ActiveInventoryNode newNode = null;
+
+ for (Relationship r : relationshipArray) {
+
+ repairedSelfLink = aaiConfig.repairSelfLink(r.getRelatedLink());
+
+ String nodeId = NodeUtils.generateUniqueShaDigest(repairedSelfLink);
+
+ if (nodeId == null) {
+
+ LOG.error(AaiUiMsgs.SKIPPING_RELATIONSHIP, r.toString());
+ processingNode.changeState(NodeProcessingState.ERROR,
+ NodeProcessingAction.NODE_IDENTITY_ERROR);
+ return false;
+ }
+
+ newNode = new ActiveInventoryNode();
+
+ String entityType = r.getRelatedTo();
+
+ if (r.getRelationshipData() != null) {
+ for (RelationshipData rd : r.getRelationshipData()) {
+ newNode.addQueryParam(rd.getRelationshipKey() + ":" + rd.getRelationshipValue());
+ }
+ }
+
+ descriptor = modelLoader.getEntityDescriptor(r.getRelatedTo());
+
+ newNode.setNodeId(nodeId);
+ newNode.setEntityType(entityType);
+ newNode.setSelfLink(repairedSelfLink);
+
+ processingNode.addOutboundNeighbor(nodeId);
+
+ if (descriptor != null) {
+
+ List<String> pkeyNames = descriptor.getPrimaryKeyAttributeName();
+
+ newNode.changeState(NodeProcessingState.SELF_LINK_UNRESOLVED,
+ NodeProcessingAction.SELF_LINK_SET);
+
+ newNode.setPrimaryKeyName(NodeUtils.concatArray(pkeyNames, "/"));
+
+ String primaryKeyValues = getRelationshipPrimaryKeyValues(r, entityType, pkeyNames);
+ newNode.setPrimaryKeyValue(primaryKeyValues);
+
+ } else {
+
+ LOG.error(AaiUiMsgs.VISUALIZATION_OUTPUT_ERROR,
+ "Failed to parse entity because OXM descriptor could not be found for type = "
+ + r.getRelatedTo());
+
+ newNode.changeState(NodeProcessingState.ERROR,
+ NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
+
+ }
+
+ if (nodeCache.putIfAbsent(nodeId, newNode) != null) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
+ "Failed to add node to nodeCache because it already exists. Node id = "
+ + newNode.getNodeId());
+ }
+ }
+
+ }
+
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Process initial state.
+ *
+ * @param nodeId the node id
+ */
+ private void processInitialState(String nodeId) {
+
+ if (nodeId == null) {
+ LOG.error(AaiUiMsgs.FAILED_TO_PROCESS_INITIAL_STATE, "Node id is null");
+ return;
+ }
+
+ ActiveInventoryNode cachedNode = nodeCache.get(nodeId);
+
+ if (cachedNode == null) {
+ LOG.error(AaiUiMsgs.FAILED_TO_PROCESS_INITIAL_STATE, "Node cannot be"
+ + " found for nodeId, " + nodeId);
+ return;
+ }
+
+ if (cachedNode.getSelfLink() == null) {
+
+ if (cachedNode.getNodeId() == null ) {
+
+ /*
+ * if the self link is null at the INIT state, which could be valid if this node is a
+ * complex attribute group which didn't originate from a self-link, but in that situation
+ * both the node id and node key should already be set.
+ */
+
+ cachedNode.changeState(NodeProcessingState.ERROR, NodeProcessingAction.NODE_IDENTITY_ERROR);
+
+ }
+
+ if (cachedNode.getNodeId() != null) {
+
+ /*
+ * This should be the success path branch if the self-link is not set
+ */
+
+ cachedNode.changeState(NodeProcessingState.SELF_LINK_RESPONSE_UNPROCESSED,
+ NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_OK);
+
+ }
+
+ } else {
+
+ if (cachedNode.hasResolvedSelfLink()) {
+ LOG.error(AaiUiMsgs.INVALID_RESOLVE_STATE_DURING_INIT);
+ cachedNode.changeState(NodeProcessingState.ERROR,
+ NodeProcessingAction.UNEXPECTED_STATE_TRANSITION);
+ } else {
+ cachedNode.changeState(NodeProcessingState.SELF_LINK_UNRESOLVED,
+ NodeProcessingAction.SELF_LINK_SET);
+ }
+ }
+ }
+
+ /**
+ * Process skeleton node.
+ *
+ * @param skeletonNode the skeleton node
+ * @param queryParams the query params
+ */
+ private void processSearchableEntity(SearchableEntity searchTargetEntity, QueryParams queryParams) {
+
+ if (searchTargetEntity == null) {
+ return;
+ }
+
+ if (searchTargetEntity.getId() == null) {
+ LOG.error(AaiUiMsgs.FAILED_TO_PROCESS_SKELETON_NODE, "Failed to process skeleton"
+ + " node because nodeId is null for node, " + searchTargetEntity.getLink());
+ return;
+ }
+
+ ActiveInventoryNode newNode = new ActiveInventoryNode();
+
+ newNode.setNodeId(searchTargetEntity.getId());
+ newNode.setEntityType(searchTargetEntity.getEntityType());
+ newNode.setPrimaryKeyName(getEntityTypePrimaryKeyName(searchTargetEntity.getEntityType()));
+ newNode.setPrimaryKeyValue(searchTargetEntity.getEntityPrimaryKeyValue());
+
+ if (newNode.getEntityType() != null && newNode.getPrimaryKeyName() != null
+ && newNode.getPrimaryKeyValue() != null) {
+ newNode.addQueryParam(
+ newNode.getEntityType() + "." + newNode.getPrimaryKeyName() + ":" + newNode.getPrimaryKeyValue());
+ }
+ /*
+ * This code may need some explanation. In any graph there will be a single root node. The root
+ * node is really the center of the universe, and for now, we are tagging the search target as
+ * the root node. Everything else in the visualization of the graph will be centered around this
+ * node as the focal point of interest.
+ *
+ * Due to it's special nature, there will only ever be one root node, and it's node depth will
+ * always be equal to zero.
+ */
+
+ if (queryParams.getSearchTargetNodeId().equals(newNode.getNodeId())) {
+ newNode.setNodeDepth(0);
+ newNode.setRootNode(true);
+ LOG.info(AaiUiMsgs.ROOT_NODE_DISCOVERED, queryParams.getSearchTargetNodeId());
+ }
+
+ newNode.setSelfLink(searchTargetEntity.getLink());
+
+ nodeCache.putIfAbsent(newNode.getNodeId(), newNode);
+ }
+
+ /**
+ * Checks for out standing work.
+ *
+ * @return true, if successful
+ */
+ private boolean hasOutStandingWork() {
+
+ int numNodesWithPendingStates = 0;
+
+ /*
+ * Force an evaluation of node depths before determining if we should limit state-based
+ * traversal or processing.
+ */
+
+ evaluateNodeDepths();
+
+ for (ActiveInventoryNode n : nodeCache.values()) {
+
+ switch (n.getState()) {
+
+ case READY:
+ case ERROR: {
+ // do nothing, these are our normal
+ // exit states
+ break;
+ }
+
+ case NEIGHBORS_UNPROCESSED: {
+
+ if (n.getNodeDepth() < VisualizationConfig.getConfig().getMaxSelfLinkTraversalDepth()) {
+ /*
+ * Only process our neighbors relationships if our current depth is less than the max
+ * depth
+ */
+ numNodesWithPendingStates++;
+ }
+
+ break;
+ }
+
+ default: {
+
+ /*
+ * for all other states, there is work to be done
+ */
+ numNodesWithPendingStates++;
+ }
+
+ }
+
+ }
+
+ LOG.debug(AaiUiMsgs.OUTSTANDING_WORK_PENDING_NODES, String.valueOf(numNodesWithPendingStates));
+
+ return (numNodesWithPendingStates > 0);
+
+ }
+
+ /**
+ * Process self links.
+ *
+ * @param skeletonNode the skeleton node
+ * @param queryParams the query params
+ */
+ public void processSelfLinks(SearchableEntity searchtargetEntity, QueryParams queryParams) {
+
+ try {
+
+ if (searchtargetEntity == null) {
+ LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, contextIdStr + " - Failed to"
+ + " processSelfLinks, searchtargetEntity is null");
+ return;
+ }
+
+ processSearchableEntity(searchtargetEntity, queryParams);
+
+ long startTimeInMs = System.currentTimeMillis();
+
+ /*
+ * wait until all transactions are complete or guard-timer expires.
+ */
+
+ long totalResolveTime = 0;
+ boolean hasOutstandingWork = hasOutStandingWork();
+ boolean outstandingWorkGuardTimerFired = false;
+ long maxGuardTimeInMs = 5000;
+ long guardTimeInMs = 0;
+ boolean foundRootNode = false;
+
+
+ /*
+ * TODO: Put a count-down-latch in place of the while loop, but if we do that then
+ * we'll need to decouple the visualization processing from the main thread so it can continue to process while
+ * the main thread is waiting on for count-down-latch gate to open. This may also be easier once we move to the
+ * VisualizationService + VisualizationContext ideas.
+ */
+
+
+ while (hasOutstandingWork || !outstandingWorkGuardTimerFired) {
+
+ if (!foundRootNode) {
+ foundRootNode = findAndMarkRootNode(queryParams);
+ }
+
+ processCurrentNodeStates(foundRootNode);
+
+ verifyOutboundNeighbors();
+
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException exc) {
+ LOG.error(AaiUiMsgs.PROCESSING_LOOP_INTERUPTED, exc.getMessage());
+ return;
+ }
+
+ totalResolveTime = (System.currentTimeMillis() - startTimeInMs);
+
+ if (!hasOutstandingWork) {
+
+ guardTimeInMs += 500;
+
+ if (guardTimeInMs > maxGuardTimeInMs) {
+ outstandingWorkGuardTimerFired = true;
+ }
+ } else {
+ guardTimeInMs = 0;
+ }
+
+ hasOutstandingWork = hasOutStandingWork();
+
+ }
+
+ long opTime = System.currentTimeMillis() - startTimeInMs;
+
+ LOG.info(AaiUiMsgs.ALL_TRANSACTIONS_RESOLVED, String.valueOf(totalResolveTime),
+ String.valueOf(totalLinksRetrieved.get()), String.valueOf(opTime));
+
+ } catch (Exception exc) {
+ LOG.error(AaiUiMsgs.VISUALIZATION_OUTPUT_ERROR, exc.getMessage());
+ }
+
+ }
+
+ /**
+ * Verify outbound neighbors.
+ */
+ private void verifyOutboundNeighbors() {
+
+ for (ActiveInventoryNode srcNode : nodeCache.values()) {
+
+ for (String targetNodeId : srcNode.getOutboundNeighbors()) {
+
+ ActiveInventoryNode targetNode = nodeCache.get(targetNodeId);
+
+ if (targetNode != null && srcNode.getNodeId() != null) {
+
+ targetNode.addInboundNeighbor(srcNode.getNodeId());
+
+ if (VisualizationConfig.getConfig().makeAllNeighborsBidirectional()) {
+ targetNode.addOutboundNeighbor(srcNode.getNodeId());
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ /**
+ * Evaluate node depths.
+ */
+ private void evaluateNodeDepths() {
+
+ int numChanged = -1;
+ int numAttempts = 0;
+
+ while (numChanged != 0) {
+
+ numChanged = 0;
+ numAttempts++;
+
+ for (ActiveInventoryNode srcNode : nodeCache.values()) {
+
+ if (srcNode.getState() == NodeProcessingState.INIT) {
+
+ /*
+ * this maybe the only state that we don't want to to process the node depth on, because
+ * typically it won't have any valid fields set, and it may remain in a partial state
+ * until we have processed the self-link.
+ */
+
+ continue;
+
+ }
+
+ for (String targetNodeId : srcNode.getOutboundNeighbors()) {
+ ActiveInventoryNode targetNode = nodeCache.get(targetNodeId);
+
+ if (targetNode != null) {
+
+ if (targetNode.changeDepth(srcNode.getNodeDepth() + 1)) {
+ numChanged++;
+ }
+ }
+ }
+
+ for (String targetNodeId : srcNode.getInboundNeighbors()) {
+ ActiveInventoryNode targetNode = nodeCache.get(targetNodeId);
+
+ if (targetNode != null) {
+
+ if (targetNode.changeDepth(srcNode.getNodeDepth() + 1)) {
+ numChanged++;
+ }
+ }
+ }
+ }
+
+ if (numAttempts >= MAX_DEPTH_EVALUATION_ATTEMPTS) {
+ LOG.info(AaiUiMsgs.MAX_EVALUATION_ATTEMPTS_EXCEEDED);
+ return;
+ }
+
+ }
+
+ if (LOG.isDebugEnabled()) {
+ if (numAttempts > 0) {
+ LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
+ "Evaluate node depths completed in " + numAttempts + " attempts");
+ } else {
+ LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
+ "Evaluate node depths completed in 0 attempts because all nodes at correct depth");
+ }
+ }
+
+ }
+
+
+ /**
+ * Gets the entity type primary key name.
+ *
+ * @param entityType the entity type
+ * @return the entity type primary key name
+ */
+
+
+ private String getEntityTypePrimaryKeyName(String entityType) {
+
+ if (entityType == null) {
+ LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE, "node primary key"
+ + " name because entity type is null");
+ return null;
+ }
+
+ OxmEntityDescriptor descriptor = loader.getEntityDescriptor(entityType);
+
+ if (descriptor == null) {
+ LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE, "oxm entity"
+ + " descriptor for entityType = " + entityType);
+ return null;
+ }
+
+ List<String> pkeyNames = descriptor.getPrimaryKeyAttributeName();
+
+ if (pkeyNames == null || pkeyNames.size() == 0) {
+ LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE, "node primary"
+ + " key because descriptor primary key names is empty");
+ return null;
+ }
+
+ return NodeUtils.concatArray(pkeyNames, "/");
+
+ }
+
+}