2 * ============LICENSE_START=======================================================
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
12 * http://www.apache.org/licenses/LICENSE-2.0
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=========================================================
21 * ECOMP is a trademark and service mark of AT&T Intellectual Property.
23 package org.onap.aai.sparky.viewandinspect.services;
25 import static java.util.concurrent.CompletableFuture.supplyAsync;
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;
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;
38 import org.apache.http.client.utils.URIBuilder;
39 import org.onap.aai.sparky.config.oxm.OxmEntityDescriptor;
40 import org.onap.aai.sparky.config.oxm.OxmModelLoader;
41 import org.onap.aai.sparky.dal.aai.ActiveInventoryDataProvider;
42 import org.onap.aai.sparky.dal.aai.config.ActiveInventoryConfig;
43 import org.onap.aai.sparky.dal.rest.OperationResult;
44 import org.onap.aai.sparky.logging.AaiUiMsgs;
45 import org.onap.aai.sparky.synchronizer.entity.SearchableEntity;
46 import org.onap.aai.sparky.util.NodeUtils;
47 import org.onap.aai.sparky.viewandinspect.config.TierSupportUiConstants;
48 import org.onap.aai.sparky.viewandinspect.config.VisualizationConfig;
49 import org.onap.aai.sparky.viewandinspect.entity.ActiveInventoryNode;
50 import org.onap.aai.sparky.viewandinspect.entity.InlineMessage;
51 import org.onap.aai.sparky.viewandinspect.entity.NodeProcessingTransaction;
52 import org.onap.aai.sparky.viewandinspect.entity.QueryParams;
53 import org.onap.aai.sparky.viewandinspect.entity.Relationship;
54 import org.onap.aai.sparky.viewandinspect.entity.RelationshipData;
55 import org.onap.aai.sparky.viewandinspect.entity.RelationshipList;
56 import org.onap.aai.sparky.viewandinspect.entity.SelfLinkDeterminationTransaction;
57 import org.onap.aai.sparky.viewandinspect.enumeration.NodeProcessingAction;
58 import org.onap.aai.sparky.viewandinspect.enumeration.NodeProcessingState;
59 import org.onap.aai.sparky.viewandinspect.task.PerformNodeSelfLinkProcessingTask;
60 import org.onap.aai.sparky.viewandinspect.task.PerformSelfLinkDeterminationTask;
61 import org.onap.aai.cl.api.Logger;
62 import org.onap.aai.cl.eelf.LoggerFactory;
64 import com.fasterxml.jackson.annotation.JsonInclude.Include;
65 import com.fasterxml.jackson.databind.JsonNode;
66 import com.fasterxml.jackson.databind.ObjectMapper;
67 import com.fasterxml.jackson.databind.PropertyNamingStrategy;
70 * The Class SelfLinkNodeCollector.
72 public class VisualizationContext {
74 private static final int MAX_DEPTH_EVALUATION_ATTEMPTS = 100;
75 private static final String DEPTH_ALL_MODIFIER = "?depth=all";
76 private static final String NODES_ONLY_MODIFIER = "?nodes-only";
77 private static final String SERVICE_INSTANCE = "service-instance";
79 private static final Logger LOG = LoggerFactory.getInstance().getLogger(
80 VisualizationContext.class);
81 private final ActiveInventoryDataProvider aaiProvider;
83 private int maxSelfLinkTraversalDepth;
84 private AtomicInteger numLinksDiscovered;
85 private AtomicInteger numSuccessfulLinkResolveFromCache;
86 private AtomicInteger numSuccessfulLinkResolveFromFromServer;
87 private AtomicInteger numFailedLinkResolve;
88 private AtomicInteger aaiWorkOnHand;
90 private ActiveInventoryConfig aaiConfig;
91 private VisualizationConfig visualizationConfig;
92 private List<String> shallowEntities;
94 private AtomicInteger totalLinksRetrieved;
96 private final long contextId;
97 private final String contextIdStr;
99 private OxmModelLoader loader;
100 private ObjectMapper mapper;
101 private InlineMessage inlineMessage = null;
103 private ExecutorService aaiExecutorService;
106 * The node cache is intended to be a flat structure indexed by a primary key to avoid needlessly
107 * re-requesting the same self-links over-and-over again, to speed up the overall render time and
108 * more importantly to reduce the network cost of determining information we already have.
110 private ConcurrentHashMap<String, ActiveInventoryNode> nodeCache;
113 * Instantiates a new self link node collector.
115 * @param loader the loader
116 * @throws Exception the exception
118 public VisualizationContext(long contextId, ActiveInventoryDataProvider aaiDataProvider,
119 ExecutorService aaiExecutorService, OxmModelLoader loader) throws Exception {
121 this.contextId = contextId;
122 this.contextIdStr = "[Context-Id=" + contextId + "]";
123 this.aaiProvider = aaiDataProvider;
124 this.aaiExecutorService = aaiExecutorService;
125 this.loader = loader;
127 this.nodeCache = new ConcurrentHashMap<String, ActiveInventoryNode>();
128 this.numLinksDiscovered = new AtomicInteger(0);
129 this.totalLinksRetrieved = new AtomicInteger(0);
130 this.numSuccessfulLinkResolveFromCache = new AtomicInteger(0);
131 this.numSuccessfulLinkResolveFromFromServer = new AtomicInteger(0);
132 this.numFailedLinkResolve = new AtomicInteger(0);
133 this.aaiWorkOnHand = new AtomicInteger(0);
135 this.aaiConfig = ActiveInventoryConfig.getConfig();
136 this.visualizationConfig = VisualizationConfig.getConfig();
137 this.shallowEntities = aaiConfig.getAaiRestConfig().getShallowEntities();
139 this.maxSelfLinkTraversalDepth = visualizationConfig.getMaxSelfLinkTraversalDepth();
141 this.mapper = new ObjectMapper();
142 mapper.setSerializationInclusion(Include.NON_EMPTY);
143 mapper.setPropertyNamingStrategy(new PropertyNamingStrategy.KebabCaseStrategy());
146 public long getContextId() {
151 * A utility method for extracting all entity-type primary key values from a provided self-link
152 * and return a set of generic-query API keys.
154 * @param parentEntityType
156 * @return a list of key values that can be used for this entity with the AAI generic-query API
158 protected List<String> extractQueryParamsFromSelfLink(String link) {
160 List<String> queryParams = new ArrayList<String>();
163 LOG.error(AaiUiMsgs.QUERY_PARAM_EXTRACTION_ERROR, "self link is null");
167 Map<String, OxmEntityDescriptor> entityDescriptors = loader.getEntityDescriptors();
171 URIBuilder urlBuilder = new URIBuilder(link);
172 String urlPath = urlBuilder.getPath();
174 OxmEntityDescriptor descriptor = null;
175 String[] urlPathElements = urlPath.split("/");
176 List<String> primaryKeyNames = null;
178 String entityType = null;
180 while (index < urlPathElements.length) {
182 descriptor = entityDescriptors.get(urlPathElements[index]);
184 if (descriptor != null) {
185 entityType = urlPathElements[index];
186 primaryKeyNames = descriptor.getPrimaryKeyAttributeName();
189 * Make sure from what ever index we matched the parent entity-type on that we can extract
190 * additional path elements for the primary key values.
193 if (index + primaryKeyNames.size() < urlPathElements.length) {
195 for (String primaryKeyName : primaryKeyNames) {
197 queryParams.add(entityType + "." + primaryKeyName + ":" + urlPathElements[index]);
200 LOG.error(AaiUiMsgs.QUERY_PARAM_EXTRACTION_ERROR,
201 "Could not extract query parametrs for entity-type = '" + entityType
202 + "' from self-link = " + link);
209 } catch (URISyntaxException exc) {
211 LOG.error(AaiUiMsgs.QUERY_PARAM_EXTRACTION_ERROR,
212 "Error extracting query parameters from self-link = " + link + ". Error = "
221 * Decode complex attribute group.
224 * @param attributeGroup the attribute group
225 * @return boolean indicating whether operation was successful (true), / failure(false).
227 public boolean decodeComplexAttributeGroup(ActiveInventoryNode ain, JsonNode attributeGroup) {
231 Iterator<Entry<String, JsonNode>> entityArrays = attributeGroup.fields();
232 Entry<String, JsonNode> entityArray = null;
234 if (entityArrays == null) {
235 LOG.error(AaiUiMsgs.ATTRIBUTE_GROUP_FAILURE, attributeGroup.toString());
236 ain.changeState(NodeProcessingState.ERROR, NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
240 while (entityArrays.hasNext()) {
242 entityArray = entityArrays.next();
244 String entityType = entityArray.getKey();
245 JsonNode entityArrayObject = entityArray.getValue();
247 if (entityArrayObject.isArray()) {
249 Iterator<JsonNode> entityCollection = entityArrayObject.elements();
250 JsonNode entity = null;
251 while (entityCollection.hasNext()) {
252 entity = entityCollection.next();
254 if (LOG.isDebugEnabled()) {
255 LOG.debug(AaiUiMsgs.DEBUG_GENERIC, "decodeComplexAttributeGroup(),"
256 + " entity = " + entity.toString());
260 * Here's what we are going to do:
262 * <li>In the ActiveInventoryNode, on construction maintain a collection of queryParams
263 * that is added to for the purpose of discovering parent->child hierarchies.
265 * <li>When we hit this block of the code then we'll use the queryParams to feed the
266 * generic query to resolve the self-link asynchronously.
268 * <li>Upon successful link determination, then and only then will we create a new node
269 * in the nodeCache and process the child
273 ActiveInventoryNode newNode = new ActiveInventoryNode();
274 newNode.setEntityType(entityType);
277 * This is partially a lie because we actually don't have a self-link for complex nodes
278 * discovered in this way.
280 newNode.setSelfLinkProcessed(true);
281 newNode.changeState(NodeProcessingState.SELF_LINK_RESPONSE_UNPROCESSED,
282 NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_OK);
285 * copy parent query params into new child
288 if (SERVICE_INSTANCE.equals(entityType)) {
291 * 1707 AAI has an issue being tracked with AAI-8932 where the generic-query cannot be
292 * resolved if all the service-instance path keys are provided. The query only works
293 * if only the service-instance key and valude are passed due to a historical reason.
294 * A fix is being worked on for 1707, and when it becomes available we can revert this
298 newNode.clearQueryParams();
303 * For all other entity-types we want to copy the parent query parameters into the new node
307 for (String queryParam : ain.getQueryParams()) {
308 newNode.addQueryParam(queryParam);
314 if (!addComplexGroupToNode(newNode, entity)) {
315 LOG.error(AaiUiMsgs.ATTRIBUTE_GROUP_FAILURE, "Failed to add child to parent for child = " + entity.toString());
318 if (!addNodeQueryParams(newNode)) {
319 LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE_NODE_ID, "Error determining node id and key for node = " + newNode.dumpNodeTree(true)
320 + " skipping relationship processing");
321 newNode.changeState(NodeProcessingState.ERROR,
322 NodeProcessingAction.NODE_IDENTITY_ERROR);
326 newNode.changeState(NodeProcessingState.NEIGHBORS_UNPROCESSED,
327 NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_OK);
333 * Order matters for the query params. We need to set the parent ones before the child
337 String selfLinkQuery =
338 aaiProvider.getGenericQueryForSelfLink(entityType, newNode.getQueryParams());
341 * <li>get the self-link
342 * <li>add it to the new node
343 * <li>generate node id
344 * <li>add node to node cache
345 * <li>add node id to parent outbound links list
346 * <li>process node children (should be automatic) (but don't query and resolve
347 * self-link as we already have all the data)
350 SelfLinkDeterminationTransaction txn = new SelfLinkDeterminationTransaction();
352 txn.setQueryString(selfLinkQuery);
353 txn.setNewNode(newNode);
354 txn.setParentNodeId(ain.getNodeId());
355 aaiWorkOnHand.incrementAndGet();
356 supplyAsync(new PerformSelfLinkDeterminationTask(txn, null, aaiProvider),
357 aaiExecutorService).whenComplete((nodeTxn, error) -> {
358 aaiWorkOnHand.decrementAndGet();
360 LOG.error(AaiUiMsgs.SELF_LINK_DETERMINATION_FAILED_GENERIC, selfLinkQuery);
363 OperationResult opResult = nodeTxn.getOpResult();
365 ActiveInventoryNode newChildNode = txn.getNewNode();
367 if (opResult != null && opResult.wasSuccessful()) {
369 if (opResult.isResolvedLinkFailure()) {
370 numFailedLinkResolve.incrementAndGet();
373 if (opResult.isResolvedLinkFromCache()) {
374 numSuccessfulLinkResolveFromCache.incrementAndGet();
377 if (opResult.isResolvedLinkFromServer()) {
378 numSuccessfulLinkResolveFromFromServer.incrementAndGet();
382 * extract the self-link from the operational result.
385 Collection<JsonNode> entityLinks = new ArrayList<JsonNode>();
386 JsonNode genericQueryResult = null;
389 NodeUtils.convertJsonStrToJsonNode(nodeTxn.getOpResult().getResult());
390 } catch (Exception exc) {
391 LOG.error(AaiUiMsgs.JSON_CONVERSION_ERROR, JsonNode.class.toString(), exc.getMessage());
394 NodeUtils.extractObjectsByKey(genericQueryResult, "resource-link",
397 String selfLink = null;
399 if (entityLinks.size() != 1) {
401 LOG.error(AaiUiMsgs.SELF_LINK_DETERMINATION_FAILED_UNEXPECTED_LINKS, String.valueOf(entityLinks.size()));
404 selfLink = ((JsonNode) entityLinks.toArray()[0]).asText();
405 selfLink = ActiveInventoryConfig.extractResourcePath(selfLink);
407 newChildNode.setSelfLink(selfLink);
408 newChildNode.setNodeId(NodeUtils.generateUniqueShaDigest(selfLink));
410 String uri = NodeUtils.calculateEditAttributeUri(selfLink);
412 newChildNode.addProperty(TierSupportUiConstants.URI_ATTR_NAME, uri);
415 ActiveInventoryNode parent = nodeCache.get(txn.getParentNodeId());
417 if (parent != null) {
418 parent.addOutboundNeighbor(newChildNode.getNodeId());
419 newChildNode.addInboundNeighbor(parent.getNodeId());
422 newChildNode.setSelfLinkPendingResolve(false);
423 newChildNode.setSelfLinkProcessed(true);
425 newChildNode.changeState(NodeProcessingState.NEIGHBORS_UNPROCESSED,
426 NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_OK);
428 nodeCache.putIfAbsent(newChildNode.getNodeId(), newChildNode);
433 LOG.error(AaiUiMsgs.SELF_LINK_RETRIEVAL_FAILED, txn.getQueryString(),
434 String.valueOf(nodeTxn.getOpResult().getResultCode()), nodeTxn.getOpResult().getResult());
435 newChildNode.setSelflinkRetrievalFailure(true);
436 newChildNode.setSelfLinkProcessed(true);
437 newChildNode.setSelfLinkPendingResolve(false);
439 newChildNode.changeState(NodeProcessingState.ERROR,
440 NodeProcessingAction.SELF_LINK_DETERMINATION_ERROR);
453 LOG.error(AaiUiMsgs.UNHANDLED_OBJ_TYPE_FOR_ENTITY_TYPE, entityType);
457 } catch (Exception exc) {
458 LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Exception caught while"
459 + " decoding complex attribute group - " + exc.getMessage());
467 * Process self link response.
469 * @param nodeId the node id
471 private void processSelfLinkResponse(String nodeId) {
473 if (nodeId == null) {
474 LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Cannot process self link"
475 + " response because nodeId is null");
479 ActiveInventoryNode ain = nodeCache.get(nodeId);
482 LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Cannot process self link response"
483 + " because can't find node for id = " + nodeId);
487 JsonNode jsonNode = null;
490 jsonNode = mapper.readValue(ain.getOpResult().getResult(), JsonNode.class);
491 } catch (Exception exc) {
492 LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to marshal json"
493 + " response str into JsonNode with error, " + exc.getLocalizedMessage());
494 ain.changeState(NodeProcessingState.ERROR,
495 NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_ERROR);
499 if (jsonNode == null) {
500 LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to parse json node str."
501 + " Parse resulted a null value.");
502 ain.changeState(NodeProcessingState.ERROR,
503 NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_ERROR);
507 Iterator<Entry<String, JsonNode>> fieldNames = jsonNode.fields();
508 Entry<String, JsonNode> field = null;
510 RelationshipList relationshipList = null;
512 while (fieldNames.hasNext()) {
514 field = fieldNames.next();
515 String fieldName = field.getKey();
517 if ("relationship-list".equals(fieldName)) {
520 relationshipList = mapper.readValue(field.getValue().toString(), RelationshipList.class);
522 if (relationshipList != null) {
523 ain.addRelationshipList(relationshipList);
526 } catch (Exception exc) {
527 LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to parse relationship-list"
528 + " attribute. Parse resulted in error, " + exc.getLocalizedMessage());
529 ain.changeState(NodeProcessingState.ERROR,
530 NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_ERROR);
536 JsonNode nodeValue = field.getValue();
538 if (nodeValue != null && nodeValue.isValueNode()) {
540 if (loader.getEntityDescriptor(fieldName) == null) {
543 * entity property name is not an entity, thus we can add this property name and value
544 * to our property set
547 ain.addProperty(fieldName, nodeValue.asText());
553 if (nodeValue.isArray()) {
555 if (loader.getEntityDescriptor(fieldName) == null) {
558 * entity property name is not an entity, thus we can add this property name and value
559 * to our property set
562 ain.addProperty(field.getKey(), nodeValue.toString());
568 ain.addComplexGroup(nodeValue);
577 String uri = NodeUtils.calculateEditAttributeUri(ain.getSelfLink());
579 ain.addProperty(TierSupportUiConstants.URI_ATTR_NAME, uri);
583 * We need a special behavior for intermediate entities from the REST model
585 * Tenants are not top level entities, and when we want to visualization
586 * their children, we need to construct keys that include the parent entity query
587 * keys, the current entity type keys, and the child keys. We'll always have the
588 * current entity and children, but never the parent entity in the current (1707) REST
591 * We have two possible solutions:
593 * 1) Try to use the custom-query approach to learn about the entity keys
594 * - this could be done, but it could be very expensive for large objects. When we do the first
595 * query to get a tenant, it will list all the in and out edges related to this entity,
596 * there is presently no way to filter this. But the approach could be made to work and it would be
597 * somewhat data-model driven, other than the fact that we have to first realize that the entity
598 * that is being searched for is not top-level entity. Once we have globally unique ids for resources
599 * this logic will not be needed and everything will be simpler. The only reason we are in this logic
600 * at all is to be able to calculate a url for the child entities so we can hash it to generate
601 * a globally unique id that can be safely used for the node.
603 * *2* Extract the keys from the pathed self-link.
604 * This is a bad solution and I don't like it but it will be fast for all resource types, as the
605 * information is already encoded in the URI. When we get to a point where we switch to a better
606 * globally unique entity identity model, then a lot of the code being used to calculate an entity url
607 * to in-turn generate a deterministic globally unique id will disappear.
610 * right now we have the following:
612 * - cloud-regions/cloud-region/{cloud-region-id}/{cloud-owner-id}/tenants/tenant/{tenant-id}
617 * For all entity types use the self-link extraction method to be consistent. Once we have a
618 * globally unique identity mechanism for entities, this logic can be revisited.
620 ain.clearQueryParams();
621 ain.addQueryParams(extractQueryParamsFromSelfLink(ain.getSelfLink()));
623 ain.changeState(NodeProcessingState.NEIGHBORS_UNPROCESSED,
624 NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_OK);
629 * Perform self link resolve.
631 * @param nodeId the node id
633 private void performSelfLinkResolve(String nodeId) {
635 if (nodeId == null) {
636 LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Resolve of self-link"
637 + " has been skipped because provided nodeId is null");
641 ActiveInventoryNode ain = nodeCache.get(nodeId);
644 LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Failed to find node with id, " + nodeId
645 + ", from node cache. Resolve self-link method has been skipped.");
649 if (!ain.isSelfLinkPendingResolve()) {
651 ain.setSelfLinkPendingResolve(true);
653 // kick off async self-link resolution
655 if (LOG.isDebugEnabled()) {
656 LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
657 "About to process node in SELF_LINK_UNPROCESSED State, link = " + ain.getSelfLink());
660 numLinksDiscovered.incrementAndGet();
662 String depthModifier = DEPTH_ALL_MODIFIER;
665 * If the current node is the search target, we want to see everything the node has to offer
666 * from the self-link and not filter it to a single node.
669 if (shallowEntities.contains(ain.getEntityType()) && !ain.isRootNode()) {
670 depthModifier = NODES_ONLY_MODIFIER;
673 NodeProcessingTransaction txn = new NodeProcessingTransaction();
674 txn.setProcessingNode(ain);
675 txn.setRequestParameters(depthModifier);
676 aaiWorkOnHand.incrementAndGet();
678 new PerformNodeSelfLinkProcessingTask(txn, depthModifier, aaiProvider, aaiConfig),
679 aaiExecutorService).whenComplete((nodeTxn, error) -> {
680 aaiWorkOnHand.decrementAndGet();
684 * an error processing the self link should probably result in the node processing
685 * state shifting to ERROR
688 nodeTxn.getProcessingNode().setSelflinkRetrievalFailure(true);
690 nodeTxn.getProcessingNode().changeState(NodeProcessingState.ERROR,
691 NodeProcessingAction.SELF_LINK_RESOLVE_ERROR);
693 nodeTxn.getProcessingNode().setSelfLinkPendingResolve(false);
697 totalLinksRetrieved.incrementAndGet();
699 OperationResult opResult = nodeTxn.getOpResult();
701 if (opResult != null && opResult.wasSuccessful()) {
703 if (opResult.isResolvedLinkFailure()) {
704 numFailedLinkResolve.incrementAndGet();
707 if (opResult.isResolvedLinkFromCache()) {
708 numSuccessfulLinkResolveFromCache.incrementAndGet();
711 if (opResult.isResolvedLinkFromServer()) {
712 numSuccessfulLinkResolveFromFromServer.incrementAndGet();
716 nodeTxn.getProcessingNode().setOpResult(opResult);
717 nodeTxn.getProcessingNode().changeState(
718 NodeProcessingState.SELF_LINK_RESPONSE_UNPROCESSED,
719 NodeProcessingAction.SELF_LINK_RESOLVE_OK);
721 nodeTxn.getProcessingNode().setSelfLinkProcessed(true);
722 nodeTxn.getProcessingNode().setSelfLinkPendingResolve(false);
725 LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, "Self Link retrieval for link,"
726 + txn.getSelfLinkWithModifiers() + ", failed with error code,"
727 + nodeTxn.getOpResult().getResultCode() + ", and message,"
728 + nodeTxn.getOpResult().getResult());
730 nodeTxn.getProcessingNode().setSelflinkRetrievalFailure(true);
731 nodeTxn.getProcessingNode().setSelfLinkProcessed(true);
733 nodeTxn.getProcessingNode().changeState(NodeProcessingState.ERROR,
734 NodeProcessingAction.SELF_LINK_RESOLVE_ERROR);
736 nodeTxn.getProcessingNode().setSelfLinkPendingResolve(false);
751 * @param nodeId the node id
753 private void processNeighbors(String nodeId) {
755 if (nodeId == null) {
756 LOG.error(AaiUiMsgs.SELF_LINK_PROCESS_NEIGHBORS_ERROR, "Failed to process"
757 + " neighbors because nodeId is null.");
761 ActiveInventoryNode ain = nodeCache.get(nodeId);
764 LOG.error(AaiUiMsgs.SELF_LINK_PROCESS_NEIGHBORS_ERROR, "Failed to process"
765 + " neighbors because node could not be found in nodeCache with id, " + nodeId);
770 * process complex attribute and relationships
773 boolean neighborsProcessedSuccessfully = true;
775 for (JsonNode n : ain.getComplexGroups()) {
776 neighborsProcessedSuccessfully &= decodeComplexAttributeGroup(ain, n);
779 for (RelationshipList relationshipList : ain.getRelationshipLists()) {
780 neighborsProcessedSuccessfully &= addSelfLinkRelationshipChildren(ain, relationshipList);
784 if (neighborsProcessedSuccessfully) {
785 ain.changeState(NodeProcessingState.READY, NodeProcessingAction.NEIGHBORS_PROCESSED_OK);
787 ain.changeState(NodeProcessingState.ERROR, NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
792 * If neighbors fail to process, there is already a call to change the state within the
793 * relationship and neighbor processing functions.
799 * Find and mark root node.
801 * @param queryParams the query params
802 * @return true, if successful
804 private boolean findAndMarkRootNode(QueryParams queryParams) {
806 for (ActiveInventoryNode cacheNode : nodeCache.values()) {
808 if (queryParams.getSearchTargetNodeId().equals(cacheNode.getNodeId())) {
809 cacheNode.setNodeDepth(0);
810 cacheNode.setRootNode(true);
811 LOG.info(AaiUiMsgs.ROOT_NODE_DISCOVERED, queryParams.getSearchTargetNodeId());
821 * Process current node states.
823 * @param rootNodeDiscovered the root node discovered
825 private void processCurrentNodeStates(boolean rootNodeDiscovered) {
827 * Force an evaluation of node depths before determining if we should limit state-based
828 * traversal or processing.
830 if (rootNodeDiscovered) {
831 evaluateNodeDepths();
834 for (ActiveInventoryNode cacheNode : nodeCache.values()) {
836 if (LOG.isDebugEnabled()) {
837 LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
838 "processCurrentNodeState(), nid = "
839 + cacheNode.getNodeId() + " , nodeDepth = " + cacheNode.getNodeDepth());
842 switch (cacheNode.getState()) {
845 processInitialState(cacheNode.getNodeId());
854 case SELF_LINK_UNRESOLVED: {
855 performSelfLinkResolve(cacheNode.getNodeId());
859 case SELF_LINK_RESPONSE_UNPROCESSED: {
860 processSelfLinkResponse(cacheNode.getNodeId());
864 case NEIGHBORS_UNPROCESSED: {
867 * We use the rootNodeDiscovered flag to ignore depth retrieval thresholds until the root
868 * node is identified. Then the evaluative depth calculations should re-balance the graph
869 * around the root node.
872 if (!rootNodeDiscovered || cacheNode.getNodeDepth() < VisualizationConfig.getConfig()
873 .getMaxSelfLinkTraversalDepth()) {
875 if (LOG.isDebugEnabled()) {
876 LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
877 "SLNC::processCurrentNodeState() -- Node at max depth,"
878 + " halting processing at current state = -- "
879 + cacheNode.getState() + " nodeId = " + cacheNode.getNodeId());
884 processNeighbors(cacheNode.getNodeId());
902 * Adds the complex group to node.
904 * @param targetNode the target node
905 * @param attributeGroup the attribute group
906 * @return true, if successful
908 private boolean addComplexGroupToNode(ActiveInventoryNode targetNode, JsonNode attributeGroup) {
910 if (attributeGroup == null) {
911 targetNode.changeState(NodeProcessingState.ERROR,
912 NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_OK);
916 RelationshipList relationshipList = null;
918 if (attributeGroup.isObject()) {
920 Iterator<Entry<String, JsonNode>> fields = attributeGroup.fields();
921 Entry<String, JsonNode> field = null;
925 while (fields.hasNext()) {
926 field = fields.next();
927 fieldName = field.getKey();
928 fieldValue = field.getValue();
930 if (fieldValue.isObject()) {
932 if (fieldName.equals("relationship-list")) {
936 mapper.readValue(field.getValue().toString(), RelationshipList.class);
938 if (relationshipList != null) {
939 targetNode.addRelationshipList(relationshipList);
942 } catch (Exception exc) {
943 LOG.error(AaiUiMsgs.SELF_LINK_JSON_PARSE_ERROR, "Failed to parse"
944 + " relationship-list attribute. Parse resulted in error, "
945 + exc.getLocalizedMessage());
946 targetNode.changeState(NodeProcessingState.ERROR,
947 NodeProcessingAction.COMPLEX_ATTRIBUTE_GROUP_PARSE_ERROR);
952 targetNode.addComplexGroup(fieldValue);
955 } else if (fieldValue.isArray()) {
956 if (LOG.isDebugEnabled()) {
957 LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
958 "Unexpected array type with a key = " + fieldName);
960 } else if (fieldValue.isValueNode()) {
961 if (loader.getEntityDescriptor(field.getKey()) == null) {
963 * property key is not an entity type, add it to our property set.
965 targetNode.addProperty(field.getKey(), fieldValue.asText());
971 } else if (attributeGroup.isArray()) {
972 if (LOG.isDebugEnabled()) {
973 LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
974 "Unexpected array type for attributeGroup = " + attributeGroup);
976 } else if (attributeGroup.isValueNode()) {
977 if (LOG.isDebugEnabled()) {
978 LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
979 "Unexpected value type for attributeGroup = " + attributeGroup);
986 public int getNumSuccessfulLinkResolveFromCache() {
987 return numSuccessfulLinkResolveFromCache.get();
990 public int getNumSuccessfulLinkResolveFromFromServer() {
991 return numSuccessfulLinkResolveFromFromServer.get();
994 public int getNumFailedLinkResolve() {
995 return numFailedLinkResolve.get();
998 public InlineMessage getInlineMessage() {
999 return inlineMessage;
1002 public void setInlineMessage(InlineMessage inlineMessage) {
1003 this.inlineMessage = inlineMessage;
1006 public void setMaxSelfLinkTraversalDepth(int depth) {
1007 this.maxSelfLinkTraversalDepth = depth;
1010 public int getMaxSelfLinkTraversalDepth() {
1011 return this.maxSelfLinkTraversalDepth;
1014 public ConcurrentHashMap<String, ActiveInventoryNode> getNodeCache() {
1019 * Gets the relationship primary key values.
1022 * @param entityType the entity type
1023 * @param pkeyNames the pkey names
1024 * @return the relationship primary key values
1026 private String getRelationshipPrimaryKeyValues(Relationship r, String entityType,
1027 List<String> pkeyNames) {
1029 StringBuilder sb = new StringBuilder(64);
1031 if (pkeyNames.size() > 0) {
1032 String primaryKey = extractKeyValueFromRelationData(r, entityType + "." + pkeyNames.get(0));
1033 if (primaryKey != null) {
1035 sb.append(primaryKey);
1038 // this should be a fatal error because unless we can
1039 // successfully retrieve all the expected keys we'll end up
1040 // with a garbage node
1041 LOG.error(AaiUiMsgs.EXTRACTION_ERROR, "ERROR: Failed to extract"
1042 + " keyName, " + entityType + "." + pkeyNames.get(0)
1043 + ", from relationship data, " + r.toString());
1047 for (int i = 1; i < pkeyNames.size(); i++) {
1049 String kv = extractKeyValueFromRelationData(r, entityType + "." + pkeyNames.get(i));
1051 sb.append("/").append(kv);
1053 // this should be a fatal error because unless we can
1054 // successfully retrieve all the expected keys we'll end up
1055 // with a garbage node
1056 LOG.error(AaiUiMsgs.EXTRACTION_ERROR, "ERROR: failed to extract keyName, "
1057 + entityType + "." + pkeyNames.get(i)
1058 + ", from relationship data, " + r.toString());
1063 return sb.toString();
1072 * Extract key value from relation data.
1075 * @param keyName the key name
1076 * @return the string
1078 private String extractKeyValueFromRelationData(Relationship r, String keyName) {
1080 RelationshipData[] rdList = r.getRelationshipData();
1082 for (RelationshipData relData : rdList) {
1084 if (relData.getRelationshipKey().equals(keyName)) {
1085 return relData.getRelationshipValue();
1093 * Determine node id and key.
1095 * @param ain the ain
1096 * @return true, if successful
1098 private boolean addNodeQueryParams(ActiveInventoryNode ain) {
1101 LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE_NODE_ID, "ActiveInventoryNode is null");
1105 List<String> pkeyNames =
1106 loader.getEntityDescriptor(ain.getEntityType()).getPrimaryKeyAttributeName();
1108 if (pkeyNames == null || pkeyNames.size() == 0) {
1109 LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE_NODE_ID, "Primary key names is empty");
1113 StringBuilder sb = new StringBuilder(64);
1115 if (pkeyNames.size() > 0) {
1116 String primaryKey = ain.getProperties().get(pkeyNames.get(0));
1117 if (primaryKey != null) {
1118 sb.append(primaryKey);
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, "ERROR: Failed to extract keyName, "
1124 + pkeyNames.get(0) + ", from entity properties");
1128 for (int i = 1; i < pkeyNames.size(); i++) {
1130 String kv = ain.getProperties().get(pkeyNames.get(i));
1132 sb.append("/").append(kv);
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, "ERROR: Failed to extract keyName, "
1138 + pkeyNames.get(i) + ", from entity properties");
1143 /*final String nodeId = NodeUtils.generateUniqueShaDigest(ain.getEntityType(),
1144 NodeUtils.concatArray(pkeyNames, "/"), sb.toString());*/
1146 //ain.setNodeId(nodeId);
1147 ain.setPrimaryKeyName(NodeUtils.concatArray(pkeyNames, "/"));
1148 ain.setPrimaryKeyValue(sb.toString());
1150 if (ain.getEntityType() != null && ain.getPrimaryKeyName() != null
1151 && ain.getPrimaryKeyValue() != null) {
1153 ain.getEntityType() + "." + ain.getPrimaryKeyName() + ":" + ain.getPrimaryKeyValue());
1164 * Adds the self link relationship children.
1166 * @param processingNode the processing node
1167 * @param relationshipList the relationship list
1168 * @return true, if successful
1170 private boolean addSelfLinkRelationshipChildren(ActiveInventoryNode processingNode,
1171 RelationshipList relationshipList) {
1173 if (relationshipList == null) {
1174 LOG.debug(AaiUiMsgs.DEBUG_GENERIC, "No relationships added to parent node = "
1175 + processingNode.getNodeId() + " because relationshipList is empty");
1176 processingNode.changeState(NodeProcessingState.ERROR,
1177 NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
1181 OxmModelLoader modelLoader = OxmModelLoader.getInstance();
1183 Relationship[] relationshipArray = relationshipList.getRelationshipList();
1184 OxmEntityDescriptor descriptor = null;
1186 if (relationshipArray != null) {
1188 ActiveInventoryNode newNode = null;
1189 String resourcePath = null;
1191 for (Relationship r : relationshipArray) {
1193 resourcePath = ActiveInventoryConfig.extractResourcePath(r.getRelatedLink());
1195 String nodeId = NodeUtils.generateUniqueShaDigest(resourcePath);
1197 if (nodeId == null) {
1199 LOG.error(AaiUiMsgs.SKIPPING_RELATIONSHIP, r.toString());
1200 processingNode.changeState(NodeProcessingState.ERROR,
1201 NodeProcessingAction.NODE_IDENTITY_ERROR);
1205 newNode = new ActiveInventoryNode();
1207 String entityType = r.getRelatedTo();
1209 if (r.getRelationshipData() != null) {
1210 for (RelationshipData rd : r.getRelationshipData()) {
1211 newNode.addQueryParam(rd.getRelationshipKey() + ":" + rd.getRelationshipValue());
1215 descriptor = modelLoader.getEntityDescriptor(r.getRelatedTo());
1217 newNode.setNodeId(nodeId);
1218 newNode.setEntityType(entityType);
1219 newNode.setSelfLink(resourcePath);
1221 processingNode.addOutboundNeighbor(nodeId);
1223 if (descriptor != null) {
1225 List<String> pkeyNames = descriptor.getPrimaryKeyAttributeName();
1227 newNode.changeState(NodeProcessingState.SELF_LINK_UNRESOLVED,
1228 NodeProcessingAction.SELF_LINK_SET);
1230 newNode.setPrimaryKeyName(NodeUtils.concatArray(pkeyNames, "/"));
1232 String primaryKeyValues = getRelationshipPrimaryKeyValues(r, entityType, pkeyNames);
1233 newNode.setPrimaryKeyValue(primaryKeyValues);
1237 LOG.error(AaiUiMsgs.VISUALIZATION_OUTPUT_ERROR,
1238 "Failed to parse entity because OXM descriptor could not be found for type = "
1239 + r.getRelatedTo());
1241 newNode.changeState(NodeProcessingState.ERROR,
1242 NodeProcessingAction.NEIGHBORS_PROCESSED_ERROR);
1246 if (nodeCache.putIfAbsent(nodeId, newNode) != null) {
1247 if (LOG.isDebugEnabled()) {
1248 LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
1249 "Failed to add node to nodeCache because it already exists. Node id = "
1250 + newNode.getNodeId());
1263 * Process initial state.
1265 * @param nodeId the node id
1267 private void processInitialState(String nodeId) {
1269 if (nodeId == null) {
1270 LOG.error(AaiUiMsgs.FAILED_TO_PROCESS_INITIAL_STATE, "Node id is null");
1274 ActiveInventoryNode cachedNode = nodeCache.get(nodeId);
1276 if (cachedNode == null) {
1277 LOG.error(AaiUiMsgs.FAILED_TO_PROCESS_INITIAL_STATE, "Node cannot be"
1278 + " found for nodeId, " + nodeId);
1282 if (cachedNode.getSelfLink() == null) {
1284 if (cachedNode.getNodeId() == null ) {
1287 * if the self link is null at the INIT state, which could be valid if this node is a
1288 * complex attribute group which didn't originate from a self-link, but in that situation
1289 * both the node id and node key should already be set.
1292 cachedNode.changeState(NodeProcessingState.ERROR, NodeProcessingAction.NODE_IDENTITY_ERROR);
1296 if (cachedNode.getNodeId() != null) {
1299 * This should be the success path branch if the self-link is not set
1302 cachedNode.changeState(NodeProcessingState.SELF_LINK_RESPONSE_UNPROCESSED,
1303 NodeProcessingAction.SELF_LINK_RESPONSE_PARSE_OK);
1309 if (cachedNode.hasResolvedSelfLink()) {
1310 LOG.error(AaiUiMsgs.INVALID_RESOLVE_STATE_DURING_INIT);
1311 cachedNode.changeState(NodeProcessingState.ERROR,
1312 NodeProcessingAction.UNEXPECTED_STATE_TRANSITION);
1314 cachedNode.changeState(NodeProcessingState.SELF_LINK_UNRESOLVED,
1315 NodeProcessingAction.SELF_LINK_SET);
1321 * Process skeleton node.
1323 * @param skeletonNode the skeleton node
1324 * @param queryParams the query params
1326 private void processSearchableEntity(SearchableEntity searchTargetEntity, QueryParams queryParams) {
1328 if (searchTargetEntity == null) {
1332 if (searchTargetEntity.getId() == null) {
1333 LOG.error(AaiUiMsgs.FAILED_TO_PROCESS_SKELETON_NODE, "Failed to process skeleton"
1334 + " node because nodeId is null for node, " + searchTargetEntity.getLink());
1338 ActiveInventoryNode newNode = new ActiveInventoryNode();
1340 newNode.setNodeId(searchTargetEntity.getId());
1341 newNode.setEntityType(searchTargetEntity.getEntityType());
1342 newNode.setPrimaryKeyName(getEntityTypePrimaryKeyName(searchTargetEntity.getEntityType()));
1343 newNode.setPrimaryKeyValue(searchTargetEntity.getEntityPrimaryKeyValue());
1345 if (newNode.getEntityType() != null && newNode.getPrimaryKeyName() != null
1346 && newNode.getPrimaryKeyValue() != null) {
1347 newNode.addQueryParam(
1348 newNode.getEntityType() + "." + newNode.getPrimaryKeyName() + ":" + newNode.getPrimaryKeyValue());
1351 * This code may need some explanation. In any graph there will be a single root node. The root
1352 * node is really the center of the universe, and for now, we are tagging the search target as
1353 * the root node. Everything else in the visualization of the graph will be centered around this
1354 * node as the focal point of interest.
1356 * Due to it's special nature, there will only ever be one root node, and it's node depth will
1357 * always be equal to zero.
1360 if (queryParams.getSearchTargetNodeId().equals(newNode.getNodeId())) {
1361 newNode.setNodeDepth(0);
1362 newNode.setRootNode(true);
1363 LOG.info(AaiUiMsgs.ROOT_NODE_DISCOVERED, queryParams.getSearchTargetNodeId());
1366 newNode.setSelfLink(searchTargetEntity.getLink());
1368 nodeCache.putIfAbsent(newNode.getNodeId(), newNode);
1372 * Checks for out standing work.
1374 * @return true, if successful
1376 private boolean hasOutStandingWork() {
1378 int numNodesWithPendingStates = 0;
1381 * Force an evaluation of node depths before determining if we should limit state-based
1382 * traversal or processing.
1385 evaluateNodeDepths();
1387 for (ActiveInventoryNode n : nodeCache.values()) {
1389 switch (n.getState()) {
1393 // do nothing, these are our normal
1398 case NEIGHBORS_UNPROCESSED: {
1400 if (n.getNodeDepth() < VisualizationConfig.getConfig().getMaxSelfLinkTraversalDepth()) {
1402 * Only process our neighbors relationships if our current depth is less than the max
1405 numNodesWithPendingStates++;
1414 * for all other states, there is work to be done
1416 numNodesWithPendingStates++;
1423 LOG.debug(AaiUiMsgs.OUTSTANDING_WORK_PENDING_NODES, String.valueOf(numNodesWithPendingStates));
1425 return (numNodesWithPendingStates > 0);
1430 * Process self links.
1432 * @param skeletonNode the skeleton node
1433 * @param queryParams the query params
1435 public void processSelfLinks(SearchableEntity searchtargetEntity, QueryParams queryParams) {
1439 if (searchtargetEntity == null) {
1440 LOG.error(AaiUiMsgs.SELF_LINK_PROCESSING_ERROR, contextIdStr + " - Failed to"
1441 + " processSelfLinks, searchtargetEntity is null");
1445 processSearchableEntity(searchtargetEntity, queryParams);
1447 long startTimeInMs = System.currentTimeMillis();
1450 * wait until all transactions are complete or guard-timer expires.
1453 long totalResolveTime = 0;
1454 boolean hasOutstandingWork = hasOutStandingWork();
1455 boolean outstandingWorkGuardTimerFired = false;
1456 long maxGuardTimeInMs = 5000;
1457 long guardTimeInMs = 0;
1458 boolean foundRootNode = false;
1462 * TODO: Put a count-down-latch in place of the while loop, but if we do that then
1463 * we'll need to decouple the visualization processing from the main thread so it can continue to process while
1464 * the main thread is waiting on for count-down-latch gate to open. This may also be easier once we move to the
1465 * VisualizationService + VisualizationContext ideas.
1469 while (hasOutstandingWork || !outstandingWorkGuardTimerFired) {
1471 if (!foundRootNode) {
1472 foundRootNode = findAndMarkRootNode(queryParams);
1475 processCurrentNodeStates(foundRootNode);
1477 verifyOutboundNeighbors();
1481 } catch (InterruptedException exc) {
1482 LOG.error(AaiUiMsgs.PROCESSING_LOOP_INTERUPTED, exc.getMessage());
1486 totalResolveTime = (System.currentTimeMillis() - startTimeInMs);
1488 if (!hasOutstandingWork) {
1490 guardTimeInMs += 500;
1492 if (guardTimeInMs > maxGuardTimeInMs) {
1493 outstandingWorkGuardTimerFired = true;
1499 hasOutstandingWork = hasOutStandingWork();
1503 long opTime = System.currentTimeMillis() - startTimeInMs;
1505 LOG.info(AaiUiMsgs.ALL_TRANSACTIONS_RESOLVED, String.valueOf(totalResolveTime),
1506 String.valueOf(totalLinksRetrieved.get()), String.valueOf(opTime));
1508 } catch (Exception exc) {
1509 LOG.error(AaiUiMsgs.VISUALIZATION_OUTPUT_ERROR, exc.getMessage());
1515 * Verify outbound neighbors.
1517 private void verifyOutboundNeighbors() {
1519 for (ActiveInventoryNode srcNode : nodeCache.values()) {
1521 for (String targetNodeId : srcNode.getOutboundNeighbors()) {
1523 ActiveInventoryNode targetNode = nodeCache.get(targetNodeId);
1525 if (targetNode != null && srcNode.getNodeId() != null) {
1527 targetNode.addInboundNeighbor(srcNode.getNodeId());
1529 if (VisualizationConfig.getConfig().makeAllNeighborsBidirectional()) {
1530 targetNode.addOutboundNeighbor(srcNode.getNodeId());
1542 * Evaluate node depths.
1544 private void evaluateNodeDepths() {
1546 int numChanged = -1;
1547 int numAttempts = 0;
1549 while (numChanged != 0) {
1554 for (ActiveInventoryNode srcNode : nodeCache.values()) {
1556 if (srcNode.getState() == NodeProcessingState.INIT) {
1559 * this maybe the only state that we don't want to to process the node depth on, because
1560 * typically it won't have any valid fields set, and it may remain in a partial state
1561 * until we have processed the self-link.
1568 for (String targetNodeId : srcNode.getOutboundNeighbors()) {
1569 ActiveInventoryNode targetNode = nodeCache.get(targetNodeId);
1571 if (targetNode != null) {
1573 if (targetNode.changeDepth(srcNode.getNodeDepth() + 1)) {
1579 for (String targetNodeId : srcNode.getInboundNeighbors()) {
1580 ActiveInventoryNode targetNode = nodeCache.get(targetNodeId);
1582 if (targetNode != null) {
1584 if (targetNode.changeDepth(srcNode.getNodeDepth() + 1)) {
1591 if (numAttempts >= MAX_DEPTH_EVALUATION_ATTEMPTS) {
1592 LOG.info(AaiUiMsgs.MAX_EVALUATION_ATTEMPTS_EXCEEDED);
1598 if (LOG.isDebugEnabled()) {
1599 if (numAttempts > 0) {
1600 LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
1601 "Evaluate node depths completed in " + numAttempts + " attempts");
1603 LOG.debug(AaiUiMsgs.DEBUG_GENERIC,
1604 "Evaluate node depths completed in 0 attempts because all nodes at correct depth");
1612 * Gets the entity type primary key name.
1614 * @param entityType the entity type
1615 * @return the entity type primary key name
1619 private String getEntityTypePrimaryKeyName(String entityType) {
1621 if (entityType == null) {
1622 LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE, "node primary key"
1623 + " name because entity type is null");
1627 OxmEntityDescriptor descriptor = loader.getEntityDescriptor(entityType);
1629 if (descriptor == null) {
1630 LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE, "oxm entity"
1631 + " descriptor for entityType = " + entityType);
1635 List<String> pkeyNames = descriptor.getPrimaryKeyAttributeName();
1637 if (pkeyNames == null || pkeyNames.size() == 0) {
1638 LOG.error(AaiUiMsgs.FAILED_TO_DETERMINE, "node primary"
1639 + " key because descriptor primary key names is empty");
1643 return NodeUtils.concatArray(pkeyNames, "/");