/** * ============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 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 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(); 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 extractQueryParamsFromSelfLink(String link) { List queryParams = new ArrayList(); if (link == null) { LOG.error(AaiUiMsgs.QUERY_PARAM_EXTRACTION_ERROR, "self link is null"); return queryParams; } Map entityDescriptors = loader.getEntityDescriptors(); try { URIBuilder urlBuilder = new URIBuilder(link); String urlPath = urlBuilder.getPath(); OxmEntityDescriptor descriptor = null; String[] urlPathElements = urlPath.split("/"); List 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> entityArrays = attributeGroup.fields(); Entry 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 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: * *
  • In the ActiveInventoryNode, on construction maintain a collection of queryParams * that is added to for the purpose of discovering parent->child hierarchies. * *
  • 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. * *
  • 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()); /** *
  • get the self-link *
  • add it to the new node *
  • generate node id *
  • add node to node cache *
  • add node id to parent outbound links list *
  • 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 entityLinks = new ArrayList(); 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> fieldNames = jsonNode.fields(); Entry 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> fields = attributeGroup.fields(); Entry 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 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 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 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 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 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, "/"); } }