| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| */ |
| |
| package org.apache.ranger.plugin.model.validation; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| |
| import com.google.common.collect.Sets; |
| import org.apache.commons.collections.CollectionUtils; |
| import org.apache.commons.lang.StringUtils; |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.apache.ranger.plugin.model.RangerPolicy; |
| import org.apache.ranger.plugin.model.RangerServiceDef; |
| import org.apache.ranger.plugin.model.RangerServiceDef.RangerResourceDef; |
| |
| import com.google.common.collect.Lists; |
| import org.apache.ranger.plugin.resourcematcher.RangerAbstractResourceMatcher; |
| import org.apache.ranger.plugin.resourcematcher.RangerPathResourceMatcher; |
| |
| public class RangerServiceDefHelper { |
| private static final Log LOG = LogFactory.getLog(RangerServiceDefHelper.class); |
| |
| static final Map<String, Delegate> _Cache = new ConcurrentHashMap<>(); |
| final Delegate _delegate; |
| |
| static public RangerServiceDef getServiceDefForPolicyFiltering(RangerServiceDef serviceDef) { |
| |
| List<RangerResourceDef> modifiedResourceDefs = new ArrayList<RangerResourceDef>(); |
| |
| for (RangerResourceDef resourceDef : serviceDef.getResources()) { |
| |
| final RangerResourceDef modifiedResourceDef; |
| |
| String matcherClassName = resourceDef.getMatcher(); |
| |
| if (RangerPathResourceMatcher.class.getName().equals(matcherClassName)) { |
| |
| Map<String, String> modifiedMatcherOptions = new HashMap<String, String>(resourceDef.getMatcherOptions()); |
| |
| modifiedMatcherOptions.put(RangerAbstractResourceMatcher.OPTION_WILD_CARD, "false"); |
| |
| modifiedResourceDef = new RangerResourceDef(resourceDef); |
| modifiedResourceDef.setMatcherOptions(modifiedMatcherOptions); |
| modifiedResourceDef.setRecursiveSupported(false); |
| |
| } else { |
| modifiedResourceDef = resourceDef; |
| } |
| |
| modifiedResourceDefs.add(modifiedResourceDef); |
| } |
| |
| return new RangerServiceDef(serviceDef.getName(), serviceDef.getDisplayName(), serviceDef.getImplClass(), serviceDef.getLabel(), |
| serviceDef.getDescription(), serviceDef.getOptions(), serviceDef.getConfigs(), modifiedResourceDefs, serviceDef.getAccessTypes(), |
| serviceDef.getPolicyConditions(), serviceDef.getContextEnrichers(), serviceDef.getEnums()); |
| } |
| |
| public static Map<String, String> getFilterResourcesForAncestorPolicyFiltering(RangerServiceDef serviceDef, Map<String, String> filterResources) { |
| |
| Map<String, String> ret = null; |
| |
| for (RangerResourceDef resourceDef : serviceDef.getResources()) { |
| |
| String matcherClassName = resourceDef.getMatcher(); |
| |
| if (RangerPathResourceMatcher.class.getName().equals(matcherClassName)) { |
| |
| String resourceDefName = resourceDef.getName(); |
| |
| final Map<String, String> resourceMatcherOptions = resourceDef.getMatcherOptions(); |
| |
| String delimiter = resourceMatcherOptions.get(RangerPathResourceMatcher.OPTION_PATH_SEPARATOR); |
| if (StringUtils.isBlank(delimiter)) { |
| delimiter = Character.toString(RangerPathResourceMatcher.DEFAULT_PATH_SEPARATOR_CHAR); |
| } |
| |
| String resourceValue = filterResources.get(resourceDefName); |
| if (StringUtils.isNotBlank(resourceValue)) { |
| if (!resourceValue.endsWith(delimiter)) { |
| resourceValue += delimiter; |
| } |
| resourceValue += RangerAbstractResourceMatcher.WILDCARD_ASTERISK; |
| |
| if (ret == null) { |
| ret = new HashMap<String, String>(); |
| } |
| ret.put(resourceDefName, resourceValue); |
| } |
| } |
| } |
| |
| return ret; |
| } |
| |
| public RangerServiceDefHelper(RangerServiceDef serviceDef) { |
| this(serviceDef, true, false); |
| } |
| |
| public RangerServiceDefHelper(RangerServiceDef serviceDef, boolean useCache) { |
| this(serviceDef, useCache, false); |
| } |
| |
| /** |
| * Intended for use when serviceDef object is not-trusted, e.g. when service-def is being created or updated. |
| * @param serviceDef |
| * @param useCache |
| */ |
| public RangerServiceDefHelper(RangerServiceDef serviceDef, boolean useCache, boolean checkForCycles) { |
| // NOTE: we assume serviceDef, its name and update time are can never by null. |
| |
| if(LOG.isDebugEnabled()) { |
| LOG.debug(String.format("==> RangerServiceDefHelper(). The RangerServiceDef: %s", serviceDef)); |
| } |
| |
| String serviceName = serviceDef.getName(); |
| Date serviceDefFreshnessDate = serviceDef.getUpdateTime(); |
| |
| Delegate delegate = null; |
| if (useCache && _Cache.containsKey(serviceName)) { |
| LOG.debug("RangerServiceDefHelper(): found delegate in cache with matching serviceName. Need to check date"); |
| Delegate that = _Cache.get(serviceName); |
| if (Objects.equals(that.getServiceFreshnessDate(), serviceDefFreshnessDate)) { |
| delegate = that; |
| LOG.debug("RangerServiceDefHelper(): cached delegate matched in date, too! Will use it now."); |
| } else { |
| LOG.debug("RangerServiceDefHelper(): cached delegate date mismatch!"); |
| } |
| } |
| if (delegate == null) { // either not found in cache or date didn't match |
| delegate = new Delegate(serviceDef, checkForCycles); |
| if (useCache) { |
| LOG.debug("RangerServiceDefHelper(): Created new delegate and put in delegate cache!"); |
| _Cache.put(serviceName, delegate); |
| } |
| } |
| _delegate = delegate; |
| } |
| |
| public RangerServiceDef getServiceDef() { |
| return _delegate._serviceDef; |
| } |
| |
| public void patchServiceDefWithDefaultValues() { |
| _delegate.patchServiceDefWithDefaultValues(); |
| } |
| |
| /** |
| * for a resource definition as follows: |
| * |
| * /-> E -> F |
| * A -> B -> C -> D |
| * \-> G -> H |
| * |
| * It would return a set with following ordered entries in it |
| * { [A B C D], [A E F], [A B G H] } |
| * |
| * @return |
| */ |
| public Set<List<RangerResourceDef>> getResourceHierarchies(Integer policyType) { |
| return _delegate.getResourceHierarchies(policyType); |
| } |
| |
| public Set<List<RangerResourceDef>> filterHierarchies_containsOnlyMandatoryResources(Integer policyType) { |
| Set<List<RangerResourceDef>> hierarchies = getResourceHierarchies(policyType); |
| Set<List<RangerResourceDef>> result = new HashSet<List<RangerResourceDef>>(hierarchies.size()); |
| for (List<RangerResourceDef> aHierarchy : hierarchies) { |
| Set<String> mandatoryResources = getMandatoryResourceNames(aHierarchy); |
| if (aHierarchy.size() == mandatoryResources.size()) { |
| result.add(aHierarchy); |
| } |
| } |
| return result; |
| } |
| |
| public Set<List<RangerResourceDef>> getResourceHierarchies(Integer policyType, Collection<String> keys) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("==> getResourceHierarchies(policyType=" + policyType + ", keys=" + StringUtils.join(keys, ",") + ")"); |
| } |
| |
| Set<List<RangerResourceDef>> ret = new HashSet<List<RangerResourceDef>>(); |
| |
| if (policyType == RangerPolicy.POLICY_TYPE_AUDIT) { |
| policyType = RangerPolicy.POLICY_TYPE_ACCESS; |
| } |
| |
| for (List<RangerResourceDef> hierarchy : getResourceHierarchies(policyType)) { |
| if (hierarchyHasAllResources(hierarchy, keys)) { |
| ret.add(hierarchy); |
| } |
| } |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("<== getResourceHierarchies(policyType=" + policyType + ", keys=" + StringUtils.join(keys, ",") + ") : " + StringUtils.join(ret, ",")); |
| } |
| return ret; |
| } |
| |
| public boolean hierarchyHasAllResources(List<RangerResourceDef> hierarchy, Collection<String> resourceNames) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("==> hierarchyHasAllResources(hierarchy=" + StringUtils.join(hierarchy, ",") + ", resourceNames=" + StringUtils.join(resourceNames, ",") + ")"); |
| } |
| boolean foundAllResourceKeys = true; |
| |
| for (String resourceKey : resourceNames) { |
| boolean found = false; |
| |
| for (RangerResourceDef resourceDef : hierarchy) { |
| if (resourceDef.getName().equals(resourceKey)) { |
| found = true; |
| break; |
| } |
| } |
| |
| if (!found) { |
| foundAllResourceKeys = false; |
| break; |
| } |
| } |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("<== hierarchyHasAllResources(hierarchy=" + StringUtils.join(hierarchy, ",") + ", resourceNames=" + StringUtils.join(resourceNames, ",") + "): " + foundAllResourceKeys); |
| } |
| return foundAllResourceKeys; |
| } |
| |
| public Set<String> getMandatoryResourceNames(List<RangerResourceDef> hierarchy) { |
| Set<String> result = new HashSet<String>(hierarchy.size()); |
| for (RangerResourceDef resourceDef : hierarchy) { |
| if (Boolean.TRUE.equals(resourceDef.getMandatory())) { |
| result.add(resourceDef.getName()); |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Set view of a hierarchy's resource names for efficient searching |
| * @param hierarchy |
| * @return |
| */ |
| public Set<String> getAllResourceNames(List<RangerResourceDef> hierarchy) { |
| Set<String> result = new HashSet<String>(hierarchy.size()); |
| for (RangerResourceDef resourceDef : hierarchy) { |
| result.add(resourceDef.getName()); |
| } |
| return result; |
| } |
| |
| /** |
| * Resources names matching the order of list of resource defs passed in. |
| * @param hierarchy |
| * @return |
| */ |
| public List<String> getAllResourceNamesOrdered(List<RangerResourceDef> hierarchy) { |
| List<String> result = new ArrayList<String>(hierarchy.size()); |
| for (RangerResourceDef resourceDef : hierarchy) { |
| result.add(resourceDef.getName()); |
| } |
| return result; |
| } |
| |
| public boolean isResourceGraphValid() { |
| return _delegate.isResourceGraphValid(); |
| } |
| |
| public List<String> getOrderedResourceNames(Collection<String> resourceNames) { |
| final List<String> ret; |
| if (resourceNames != null) { |
| ret = new ArrayList<>(); |
| for (String orderedName : _delegate.getAllOrderedResourceNames()) { |
| for (String resourceName : resourceNames) { |
| if (StringUtils.equals(orderedName, resourceName) && !ret.contains(resourceName)) { |
| ret.add(resourceName); |
| break; |
| } |
| } |
| } |
| } else { |
| ret = Collections.EMPTY_LIST; |
| } |
| return ret; |
| } |
| |
| /** |
| * Not designed for public access. Package level only for testability. |
| */ |
| static class Delegate { |
| final RangerServiceDef _serviceDef; |
| final Map<Integer, Set<List<RangerResourceDef>>> _hierarchies = new HashMap<>(); |
| final Date _serviceDefFreshnessDate; |
| final String _serviceName; |
| final boolean _checkForCycles; |
| final boolean _valid; |
| final List<String> _orderedResourceNames; |
| final static Set<List<RangerResourceDef>> EMPTY_RESOURCE_HIERARCHY = Collections.unmodifiableSet(new HashSet<List<RangerResourceDef>>()); |
| |
| |
| public Delegate(RangerServiceDef serviceDef, boolean checkForCycles) { |
| // NOTE: we assume serviceDef, its name and update time are can never by null. |
| _serviceDef = serviceDef; |
| _serviceName = serviceDef.getName(); |
| _serviceDefFreshnessDate = serviceDef.getUpdateTime(); |
| _checkForCycles = checkForCycles; |
| |
| boolean isValid = true; |
| for(Integer policyType : RangerPolicy.POLICY_TYPES) { |
| List<RangerResourceDef> resources = getResourceDefs(serviceDef, policyType); |
| DirectedGraph graph = createGraph(resources); |
| |
| if(graph != null) { |
| Map<String, RangerResourceDef> resourceDefMap = getResourcesAsMap(resources); |
| if (isValid(graph, resourceDefMap)) { |
| Set<List<RangerResourceDef>> hierarchies = getHierarchies(graph, resourceDefMap); |
| _hierarchies.put(policyType, Collections.unmodifiableSet(hierarchies)); |
| } else { |
| isValid = false; |
| _hierarchies.put(policyType, EMPTY_RESOURCE_HIERARCHY); |
| } |
| } else { |
| _hierarchies.put(policyType, EMPTY_RESOURCE_HIERARCHY); |
| } |
| } |
| |
| if (isValid) { |
| _orderedResourceNames = buildSortedResourceNames(); |
| } else { |
| _orderedResourceNames = new ArrayList<>(); |
| } |
| |
| _valid = isValid; |
| if (LOG.isDebugEnabled()) { |
| String message = String.format("Found [%d] resource hierarchies for service [%s] update-date[%s]: %s", _hierarchies.size(), _serviceName, |
| _serviceDefFreshnessDate == null ? null : _serviceDefFreshnessDate.toString(), _hierarchies); |
| LOG.debug(message); |
| } |
| } |
| |
| public void patchServiceDefWithDefaultValues() { |
| for(int policyType : RangerPolicy.POLICY_TYPES) { |
| Set<List<RangerResourceDef>> resourceHierarchies = getResourceHierarchies(policyType); |
| for (List<RangerResourceDef> resourceHierarchy : resourceHierarchies) { |
| for (int index = 0; index < resourceHierarchy.size(); index++) { |
| RangerResourceDef resourceDef = resourceHierarchy.get(index); |
| if (!Boolean.TRUE.equals(resourceDef.getIsValidLeaf())) { |
| resourceDef.setIsValidLeaf(index == resourceHierarchy.size()-1); |
| } |
| } |
| } |
| } |
| } |
| |
| public Set<List<RangerResourceDef>> getResourceHierarchies(Integer policyType) { |
| if(policyType == null) { |
| policyType = RangerPolicy.POLICY_TYPE_ACCESS; |
| } |
| |
| Set<List<RangerResourceDef>> ret = _hierarchies.get(policyType); |
| |
| if(ret == null) { |
| ret = EMPTY_RESOURCE_HIERARCHY; |
| } |
| |
| return ret; |
| } |
| |
| public String getServiceName() { |
| return _serviceName; |
| } |
| |
| public Date getServiceFreshnessDate() { |
| return _serviceDefFreshnessDate; |
| } |
| |
| public boolean isResourceGraphValid() { |
| return _valid; |
| } |
| /** |
| * Builds a directed graph where each resource is node and arc goes from parent level to child level |
| * |
| * @param resourceDefs |
| * @return |
| */ |
| DirectedGraph createGraph(List<RangerResourceDef> resourceDefs) { |
| DirectedGraph graph = null; |
| |
| if(CollectionUtils.isNotEmpty(resourceDefs)) { |
| graph = new DirectedGraph(); |
| |
| for (RangerResourceDef resourceDef : resourceDefs) { |
| String name = resourceDef.getName(); |
| |
| graph.add(name); |
| String parent = resourceDef.getParent(); |
| if (StringUtils.isNotEmpty(parent)) { |
| graph.addArc(parent, name); |
| } |
| } |
| } |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Created graph for resources: " + graph); |
| } |
| return graph; |
| } |
| |
| List<RangerResourceDef> getResourceDefs(RangerServiceDef serviceDef, Integer policyType) { |
| final List<RangerResourceDef> resourceDefs; |
| |
| if(policyType == null || policyType == RangerPolicy.POLICY_TYPE_ACCESS) { |
| resourceDefs = serviceDef.getResources(); |
| } else if(policyType == RangerPolicy.POLICY_TYPE_DATAMASK) { |
| if(serviceDef.getDataMaskDef() != null) { |
| resourceDefs = serviceDef.getDataMaskDef().getResources(); |
| } else { |
| resourceDefs = null; |
| } |
| } else if(policyType == RangerPolicy.POLICY_TYPE_ROWFILTER) { |
| if(serviceDef.getRowFilterDef() != null) { |
| resourceDefs = serviceDef.getRowFilterDef().getResources(); |
| } else { |
| resourceDefs = null; |
| } |
| } else { // unknown policyType; use all resources |
| resourceDefs = serviceDef.getResources(); |
| } |
| |
| return resourceDefs; |
| } |
| /** |
| * A valid resource graph is a forest, i.e. a disjoint union of trees. In our case, given that node can have only one "parent" node, we can detect this validity simply by ensuring that |
| * the resource graph has: |
| * - at least one sink AND |
| * - and least one source. |
| * |
| * A more direct method would have been ensure that the resulting graph does not have any cycles. |
| * |
| * @param graph |
| * |
| * @return |
| */ |
| boolean isValid(DirectedGraph graph, Map<String, RangerResourceDef> resourceDefMap) { |
| boolean ret = true; |
| Set<String> sources = graph.getSources(); |
| Set<String> sinks = graph.getSinks(); |
| |
| if (CollectionUtils.isEmpty(sources) || CollectionUtils.isEmpty(sinks)) { |
| ret = false; |
| } else { |
| List<String> cycle = _checkForCycles ? graph.getACycle(sources, sinks) : null; |
| if (cycle == null) { |
| for (String sink : sinks) { |
| RangerResourceDef sinkResourceDef = resourceDefMap.get(sink); |
| if (Boolean.FALSE.equals(sinkResourceDef.getIsValidLeaf())) { |
| LOG.error("Error in path: sink node:[" + sink + "] is not leaf node"); |
| ret = false; |
| break; |
| } |
| } |
| } else { |
| LOG.error("Graph contains cycle! - " + cycle); |
| ret = false; |
| } |
| } |
| |
| return ret; |
| } |
| |
| /** |
| * Returns all valid resource hierarchies for the configured resource-defs. Behavior is undefined if it is called on and invalid graph. Use <code>isValid</code> to check validation first. |
| * |
| * @param graph |
| * @param resourceMap |
| * @return |
| */ |
| Set<List<RangerResourceDef>> getHierarchies(DirectedGraph graph, Map<String, RangerResourceDef> resourceMap) { |
| Set<List<String>> hierarchies = new HashSet<>(); |
| Set<String> sources = graph.getSources(); |
| Set<String> sinks = graph.getSinks(); |
| |
| for (String source : sources) { |
| /* |
| * A disconnected node, i.e. one that does not have any arc coming into or out of it is a hierarchy in itself! |
| * A source by definition does not have any arcs coming into it. So if it also doesn't have any neighbors then we know |
| * it is a disconnected node. |
| */ |
| if (!graph.hasNeighbors(source)) { |
| hierarchies.add(Lists.newArrayList(source)); |
| } else { |
| for (String sink : sinks) { |
| List<String> path = graph.getAPath(source, sink, new HashSet<String>()); |
| |
| if (!path.isEmpty()) { |
| hierarchies.add(path); |
| |
| List<String> workingPath = new ArrayList<String>(); |
| |
| for (int index = 0, pathSize = path.size(); index < pathSize -1; index++) { |
| String node = path.get(index); |
| |
| workingPath.add(node); |
| |
| if (Boolean.TRUE.equals(resourceMap.get(node).getIsValidLeaf())) { |
| hierarchies.add(new ArrayList<String>(workingPath)); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| return convertHierarchies(hierarchies, resourceMap); |
| } |
| |
| Set<List<RangerResourceDef>> convertHierarchies(Set<List<String>> hierarchies, Map<String, RangerResourceDef> resourceMap) { |
| Set<List<RangerResourceDef>> result = new HashSet<List<RangerResourceDef>>(hierarchies.size()); |
| for (List<String> hierarchy : hierarchies) { |
| List<RangerResourceDef> resourceList = new ArrayList<RangerResourceDef>(hierarchy.size()); |
| for (String name : hierarchy) { |
| RangerResourceDef def = resourceMap.get(name); |
| resourceList.add(def); |
| } |
| result.add(resourceList); |
| } |
| return result; |
| } |
| |
| /** |
| * Converts resource list to resource map for efficient querying |
| * |
| * @param resourceList - is guaranteed to be non-null and non-empty |
| * @return |
| */ |
| Map<String, RangerResourceDef> getResourcesAsMap(List<RangerResourceDef> resourceList) { |
| Map<String, RangerResourceDef> map = new HashMap<String, RangerResourceDef>(resourceList.size()); |
| for (RangerResourceDef resourceDef : resourceList) { |
| map.put(resourceDef.getName(), resourceDef); |
| } |
| return map; |
| } |
| |
| List<String> getAllOrderedResourceNames() { |
| return this._orderedResourceNames; |
| } |
| |
| private static class ResourceNameLevel implements Comparable<ResourceNameLevel> { |
| private String resourceName; |
| private int level; |
| |
| ResourceNameLevel(String resourceName, int level) { |
| this.resourceName = resourceName; |
| this.level = level; |
| } |
| |
| @Override |
| public int compareTo(ResourceNameLevel other) { |
| return Integer.compare(this.level, other.level); |
| } |
| } |
| |
| private List<String> buildSortedResourceNames() { |
| final List<String> ret = new ArrayList<>(); |
| |
| boolean isValid = true; |
| List<ResourceNameLevel> resourceNameLevels = new ArrayList<>(); |
| for (RangerResourceDef resourceDef : _serviceDef.getResources()) { |
| String name = resourceDef.getName(); |
| Integer level = resourceDef.getLevel(); |
| if (name != null && level != null) { |
| ResourceNameLevel resourceNameLevel = new ResourceNameLevel(name, level); |
| resourceNameLevels.add(resourceNameLevel); |
| } else { |
| LOG.error("Incorrect resourceDef:[name=" + name + "]"); |
| isValid = false; |
| break; |
| } |
| } |
| |
| if (isValid) { |
| Collections.sort(resourceNameLevels); |
| |
| for (ResourceNameLevel resourceNameLevel : resourceNameLevels) { |
| ret.add(resourceNameLevel.resourceName); |
| } |
| } |
| return ret; |
| } |
| } |
| |
| /** |
| * Limited DAG implementation to analyze resource graph for a service. Not designed for public access. Package level only for testability. |
| */ |
| static class DirectedGraph { |
| Map<String, Set<String>> _nodes = new HashMap<>(); |
| |
| /** |
| * Add a node to the graph |
| * |
| * @param node |
| */ |
| void add(String node) { |
| if (node == null) { |
| throw new IllegalArgumentException("Node can't be null!"); |
| } else if (!_nodes.containsKey(node)) { // don't mess with a node's neighbors if it already exists in the graph |
| _nodes.put(node, new HashSet<String>()); |
| } |
| } |
| |
| /** |
| * Connects node "from" to node "to". Being a directed graph, after this call "to" will be in the list of neighbor's of "from". While the converse need not be true. |
| * |
| * @param from |
| * @param to |
| */ |
| void addArc(String from, String to) { |
| // connecting two nodes, implicitly adds nodes to the graph if they aren't already in it |
| if (!_nodes.containsKey(from)) { |
| add(from); |
| } |
| if (!_nodes.containsKey(to)) { |
| add(to); |
| } |
| _nodes.get(from).add(to); |
| } |
| |
| /** |
| * Returns true if "to" is in the list of neighbors of "from" |
| * |
| * @param from |
| * @param to |
| * @return |
| */ |
| boolean hasArc(String from, String to) { |
| return _nodes.containsKey(from) && _nodes.containsKey(to) && _nodes.get(from).contains(to); |
| } |
| |
| /** |
| * Returns true if the node "from" has any neighbor. |
| * @param from |
| * @return |
| */ |
| boolean hasNeighbors(String from) { |
| return _nodes.containsKey(from) && !_nodes.get(from).isEmpty(); |
| } |
| /** |
| * Return the set of nodes with in degree of 0, i.e. those that are not in any other nodes' list of neighbors |
| * |
| * @return |
| */ |
| Set<String> getSources() { |
| Set<String> sources = new HashSet<>(_nodes.keySet()); |
| for (Map.Entry<String, Set<String>> entry : _nodes.entrySet()) { |
| Set<String> nbrs = entry.getValue(); // can never be null |
| sources.removeAll(nbrs); // A source in a DAG can't be a neighbor of any other node |
| } |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Returning sources: " + sources); |
| } |
| return sources; |
| } |
| |
| /** |
| * Returns the set of nodes with out-degree of 0, i.e. those nodes whose list of neighbors is empty |
| * |
| * @return |
| */ |
| Set<String> getSinks() { |
| Set<String> sinks = new HashSet<>(); |
| for (Map.Entry<String, Set<String>> entry : _nodes.entrySet()) { |
| Set<String> nbrs = entry.getValue(); |
| if (nbrs.isEmpty()) { // A sink in a DAG doesn't have any neighbor |
| String node = entry.getKey(); |
| sinks.add(node); |
| } |
| } |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Returning sinks: " + sinks); |
| } |
| return sinks; |
| } |
| |
| List<String> getACycle(Set<String> sources, Set<String> sinks) { |
| List<String> ret = null; |
| Set<String> nonSourceOrSinkNodes = Sets.difference(_nodes.keySet(), Sets.union(sources, sinks)); |
| for (String node : nonSourceOrSinkNodes) { |
| List<String> seenNodes = new ArrayList<>(); |
| seenNodes.add(node); |
| ret = findCycle(node, seenNodes); |
| if (ret != null) { |
| break; |
| } |
| } |
| return ret; |
| } |
| |
| /** |
| * Does a depth first traversal of a graph starting from given node. Returns a sequence of nodes that form first cycle or null if no cycle is found. |
| * |
| * @param node Start node |
| * @param seenNodes List of nodes seen thus far |
| * @return list of nodes comprising first cycle in graph; null if no cycle was found |
| */ |
| List<String> findCycle(String node, List<String> seenNodes) { |
| List<String> ret = null; |
| Set<String> nbrs = _nodes.get(node); |
| for (String nbr : nbrs) { |
| boolean foundCycle = seenNodes.contains(nbr); |
| seenNodes.add(nbr); |
| if (foundCycle) { |
| ret = seenNodes; |
| break; |
| } else { |
| ret = findCycle(nbr, seenNodes); |
| if (ret != null) { |
| break; |
| } |
| } |
| } |
| return ret; |
| } |
| /** |
| * Attempts to do a depth first traversal of a graph and returns the resulting path. Note that there could be several paths that connect node "from" to node "to". |
| * |
| * @param from |
| * @param to |
| * @return |
| */ |
| List<String> getAPath(String from, String to, Set<String> alreadyVisited) { |
| List<String> path = new ArrayList<String>(_nodes.size()); |
| if (_nodes.containsKey(from) && _nodes.containsKey(to)) { // one can never reach non-existent nodes |
| if (hasArc(from, to)) { |
| path.add(from); |
| path.add(to); |
| } else { |
| alreadyVisited.add(from); |
| Set<String> nbrs = _nodes.get(from); |
| for (String nbr : nbrs) { |
| if (!alreadyVisited.contains(nbr)) { |
| List<String> subPath = getAPath(nbr, to, alreadyVisited); |
| if (!subPath.isEmpty()) { |
| path.add(from); |
| path.addAll(subPath); |
| } |
| } |
| } |
| } |
| } |
| return path; |
| } |
| |
| @Override |
| public boolean equals(Object object) { |
| if (object == this) { |
| return true; |
| } |
| if (object == null || object.getClass() != this.getClass()) { |
| return false; |
| } |
| DirectedGraph that = (DirectedGraph)object; |
| return Objects.equals(this._nodes, that._nodes); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hashCode(_nodes); |
| } |
| |
| @Override |
| public String toString() { |
| return "_nodes=" + Objects.toString(_nodes); |
| } |
| } |
| } |