/* * 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.felix.dm.diagnostics; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Stack; import java.util.stream.Collectors; import org.apache.felix.dm.Component; import org.apache.felix.dm.ComponentDeclaration; import org.apache.felix.dm.ComponentDependencyDeclaration; import org.apache.felix.dm.DependencyManager; import org.osgi.framework.Bundle; /** * The dependency graph is a view of all components managed by the dependency manager * and of their dependencies. Using this API you can get the dependencies of a given component, * the components providing a given service, the circular dependencies that might exist. * * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a> * */ public class DependencyGraph { /** * Use this to specify which components the dependency graph should contain */ public enum ComponentState { ALL, UNREGISTERED }; /** * Use this to specify which dependencies the graph should contain */ public enum DependencyState { ALL, ALL_UNAVAILABLE, REQUIRED_UNAVAILABLE }; private static final String SERVICE = "service"; private static final String CONFIGURATION = "configuration"; private static final String BUNDLE = "bundle"; private static final String RESOURCE = "resource"; private Map<ComponentDeclaration, DependencyGraphNode> m_componentToNode = new HashMap<>(); private Map<ComponentDependencyDeclaration, DependencyGraphNode> m_dependencyToNode = new HashMap<>(); private List<List<DependencyGraphNode>> m_circularDependencies = new ArrayList<>(); private Map<DependencyGraphNode, DependencyGraphNode> m_parent = new HashMap<>(); private ComponentState m_componentState = ComponentState.ALL; private DependencyState m_dependencyState = DependencyState.ALL; private DependencyGraph(ComponentState componentState, DependencyState dependencyState) { m_componentState = componentState; m_dependencyState = dependencyState; buildComponentNodes(); buildDependecyNodesAndEdges(); } private void buildComponentNodes() { List<DependencyManager> dependencyManagers = DependencyManager.getDependencyManagers(); for(DependencyManager dm : dependencyManagers) { List<Component> components = dm.getComponents(); for(Component c : components) { ComponentDeclaration cd = c.getComponentDeclaration(); if(componentMustBeAddedToGraph(cd)) { m_componentToNode.put(cd, new ComponentNode(cd)); } } } } private boolean componentMustBeAddedToGraph(ComponentDeclaration cd) { if(m_componentState == ComponentState.ALL) { return true; } else if(m_componentState == ComponentState.UNREGISTERED) { return cd.getState() == ComponentDeclaration.STATE_UNREGISTERED; } return false; } private void buildDependecyNodesAndEdges() { for(DependencyGraphNode node : m_componentToNode.values()) { ComponentNode componentNode = (ComponentNode)node; ComponentDependencyDeclaration[] dependencyDeclarations = componentNode.getComponentDeclaration().getComponentDependencies(); for(ComponentDependencyDeclaration cdd : dependencyDeclarations) { if(dependencyMustBeAddedToGraph(cdd)) { DependencyNode dependencyNode = new DependencyNode(cdd); m_dependencyToNode.put(cdd, dependencyNode); // add edges from the component node to newly created dependency node componentNode.addSuccessor(dependencyNode); // add edges from the newly created dependency node to the components // providing those dependencies (only applicable to service dependencies) List<ComponentNode> providerComponents = getProviderComponents(dependencyNode); for(ComponentNode p : providerComponents) { dependencyNode.addSuccessor(p); } } } } } private List<ComponentNode> getProviderComponents(DependencyNode dependencyNode) { List<ComponentNode> result = new ArrayList<>(); ComponentDependencyDeclaration cdd = dependencyNode.getDependencyDeclaration(); if(!SERVICE.equals(cdd.getType())) { return result; } for(DependencyGraphNode n : m_componentToNode.values()) { ComponentNode componentNode = (ComponentNode)n; if(componentProvidesDependency(componentNode, dependencyNode)) { result.add(componentNode); } } return result; } private boolean componentProvidesDependency(ComponentNode componentNode, DependencyNode dependencyNode) { ComponentDeclaration cd = componentNode.getComponentDeclaration(); String dependencyName = dependencyNode.getDependencyDeclaration().getName(); String simpleName = getSimpleName(dependencyName); Properties properties = parseProperties(dependencyName); String componentName = cd.getName(); String simpleComponentName = componentName; int cuttOff = simpleComponentName.indexOf("("); if (cuttOff != -1) { simpleComponentName = simpleComponentName.substring(0, cuttOff).trim(); } for (String serviceName : simpleComponentName.split(",")) { if (simpleName.equals(serviceName.trim()) && doPropertiesMatch(properties, parseProperties(componentName))) { return true; } } return false; } private boolean dependencyMustBeAddedToGraph(ComponentDependencyDeclaration cdd) { if(m_dependencyState == DependencyState.ALL) { return true; } else if(m_dependencyState == DependencyState.ALL_UNAVAILABLE) { return (cdd.getState() == ComponentDependencyDeclaration.STATE_UNAVAILABLE_REQUIRED) || (cdd.getState() == ComponentDependencyDeclaration.STATE_UNAVAILABLE_OPTIONAL); } else if(m_dependencyState == DependencyState.REQUIRED_UNAVAILABLE) { return cdd.getState() == ComponentDependencyDeclaration.STATE_UNAVAILABLE_REQUIRED; } return false; } /** * Build the dependency graph. It will contain all the components managed by the dependency manager, provided * that those components are in the given state, and all dependencies of their dependencies, provided that the * dependencies are in the given state. * * <p>This implementation currently only builds a graph of unregistered components and * required unavailable dependencies. * * @param componentState Include only the components in this state * @param dependencyState Include only the dependencies in this state * @return */ public static DependencyGraph getGraph(ComponentState componentState, DependencyState dependencyState) { return new DependencyGraph(componentState, dependencyState); } /** * Returns the list of components in the graph * @return the list of components in the graph */ public List<ComponentDeclaration> getAllComponents() { return new ArrayList<ComponentDeclaration>(m_componentToNode.keySet()); } /** * Returns a list all dependencies in the graph * @return the list of all dependencies in the graph */ public List<ComponentDependencyDeclaration> getAllDependencies() { return new ArrayList<ComponentDependencyDeclaration>(m_dependencyToNode.keySet()); } /** * For a given component declaration, it returns a list of its dependencies in the state * specified when the graph was built. * @param componentDeclaration * @return the list of dependencies or null if the component declaration is not in the graph */ public List<ComponentDependencyDeclaration> getDependecies(ComponentDeclaration componentDeclaration) { List<ComponentDependencyDeclaration> result = new ArrayList<>(); DependencyGraphNode node = m_componentToNode.get(componentDeclaration); if(node == null) { return null; } for(DependencyGraphNode s : node.getSuccessors()) { result.add( ((DependencyNode)s).getDependencyDeclaration() ); } return result; } /** * Returns the list of components that provide the given dependency. This only returns the components * managed by the dependency manager that are in the state specified when the graph was built. The * dependency can only be a service dependency. * * @param dependency * @return the list of components providing this dependency or null if the dependency declaration is * not in the graph */ public List<ComponentDeclaration> getProviders(ComponentDependencyDeclaration dependency) { List<ComponentDeclaration> result = new ArrayList<>(); DependencyGraphNode node = m_dependencyToNode.get(dependency); if(node == null) { return null; } for(DependencyGraphNode s : node.getSuccessors()) { result.add(((ComponentNode)s).getComponentDeclaration()); } return result; } /** * Returns the list of circular dependencies in the graph * @return the list of circular dependencies */ public List<CircularDependency> getCircularDependencies() { List<CircularDependency> result = new ArrayList<CircularDependency>(); for(DependencyGraphNode n : m_componentToNode.values()) { if(n.isUndiscovered()) { depthFirstSearch(n); } } for(List<DependencyGraphNode> cycle : m_circularDependencies) { CircularDependency circularDependency = new CircularDependency(); for(DependencyGraphNode n : cycle) { if(n instanceof ComponentNode) { circularDependency.addComponent(((ComponentNode) n).getComponentDeclaration()); } } result.add(circularDependency); } return result; } private void depthFirstSearch(DependencyGraphNode n) { n.setState(DependencyGraphNode.DependencyGraphNodeState.DISCOVERED); for(DependencyGraphNode s : n.getSuccessors()) { if(s.isUndiscovered()) { m_parent.put(s, n); depthFirstSearch(s); } else if(s.isDiscovered()) { addCycle(n, s); } } n.setState(DependencyGraphNode.DependencyGraphNodeState.PROCESSED); } private void addCycle(DependencyGraphNode n, DependencyGraphNode s) { List<DependencyGraphNode> cycle = new ArrayList<>(); Stack<DependencyGraphNode> stack = new Stack<>(); stack.push(s); for(DependencyGraphNode p = n; p != s; p = m_parent.get(p)) { stack.push(p); } stack.push(s); while(!stack.isEmpty()) { cycle.add(stack.pop()); } m_circularDependencies.add(cycle); } /** * Returns all the missing dependencies of a given type. * @param type The type of the dependencies to be returned. This can be either one of the types * known by the DependencyManager (service, bundle, configuration, resource), * a user defined type or null, in which case all missing dependencies must be returned. * * @return The missing dependencies of the given type or all the missing dependencies. */ public List<MissingDependency> getMissingDependencies(String type) { List<MissingDependency> result = new ArrayList<>(); // get all dependency nodes that have no out-going edges List<DependencyNode> missingDependencies = new ArrayList<>(); for(DependencyGraphNode node : m_dependencyToNode.values()) { DependencyNode dependencyNode = (DependencyNode)node; if(!dependencyNode.isUnavailable()) { continue; } if( (type != null) && (!dependencyNode.getDependencyDeclaration().getType().equals(type)) ) { continue; } if (dependencyNode.getSuccessors().isEmpty()) { missingDependencies.add(dependencyNode); } } for(DependencyNode node : missingDependencies) { for(DependencyGraphNode p : node.getPredecessors()) { ComponentNode componentNode = (ComponentNode)p; Bundle bundle = componentNode.getComponentDeclaration().getBundleContext().getBundle(); MissingDependency missingDependency = new MissingDependency( node.getDependencyDeclaration().getName(), node.getDependencyDeclaration().getType(), bundle.getSymbolicName()); result.add(missingDependency); } } return result; } /** * Returns all custom missing dependencies. Custom dependencies are not 'out of the box' dependencies (service, resource, * bundle, configuration). * * @return The missing dependencies which are not 'out of the box dependencies'. */ public List<MissingDependency> getMissingCustomDependencies() { List<MissingDependency> missingDependencies = getMissingDependencies(null); return missingDependencies.stream().filter(m -> !isOutOfTheBoxDependencyType(m.getType())).collect(Collectors.toList()); } private boolean isOutOfTheBoxDependencyType(String type) { return SERVICE.equals(type) || CONFIGURATION.equals(type) || BUNDLE.equals(type) || RESOURCE.equals(type); } private String getSimpleName(String name) { int cuttOff = name.indexOf("("); if (cuttOff != -1) { return name.substring(0, cuttOff).trim(); } return name.trim(); } private Properties parseProperties(String name) { Properties result = new Properties(); int cuttOff = name.indexOf("("); if (cuttOff != -1) { String propsText = name.substring(cuttOff + 1, name.indexOf(")")); String[] split = propsText.split(","); for (String prop : split) { String[] kv = prop.split("="); if (kv.length == 2) { result.put(kv[0], kv[1]); } } } return result; } private boolean doPropertiesMatch(Properties need, Properties provide) { for (Entry<Object, Object> entry : need.entrySet()) { Object prop = provide.get(entry.getKey()); if (prop == null || !prop.equals(entry.getValue())) { return false; } } return true; } }