/**
* ***************************************************************************
* Copyright (c) 2010 Qcadoo Limited
* Project: Qcadoo Framework
* Version: 1.4
*
* This file is part of Qcadoo.
*
* Qcadoo is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation; either version 3 of the License,
* or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
* ***************************************************************************
*/
package com.qcadoo.plugin.internal.dependencymanager;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jgrapht.DirectedGraph;
import org.jgrapht.alg.CycleDetector;
import org.jgrapht.graph.DefaultDirectedGraph;
import org.jgrapht.graph.DefaultEdge;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.qcadoo.plugin.api.Plugin;
import com.qcadoo.plugin.api.PluginAccessor;
import com.qcadoo.plugin.api.PluginDependencyInformation;
import com.qcadoo.plugin.api.PluginDependencyResult;
import com.qcadoo.plugin.internal.api.PluginDependencyManager;
import com.qcadoo.plugin.internal.api.PluginDependencyResultImpl;
@Service
public final class DefaultPluginDependencyManager implements PluginDependencyManager {
private static final Logger LOG = LoggerFactory.getLogger(DefaultPluginDependencyManager.class);
@Autowired
private PluginAccessor pluginAccessor;
@Override
public PluginDependencyResult getDependenciesToEnable(final List<Plugin> plugins,
final PluginStatusResolver pluginStatusResolver) {
Set<String> marked = new HashSet<String>();
DirectedGraph<String, DefaultEdge> graph = new DefaultDirectedGraph<String, DefaultEdge>(DefaultEdge.class);
PluginDependencyResult pluginDependencyResult = getDependenciesToEnable(plugins, marked, graph, pluginStatusResolver);
CycleDetector<String, DefaultEdge> cycleDetector = new CycleDetector<String, DefaultEdge>(graph);
if (cycleDetector.detectCycles()) {
LOG.warn("Dependency cycle detected: " + cycleDetector.findCycles());
return PluginDependencyResultImpl.cyclicDependencies();
} else {
return pluginDependencyResult;
}
}
private PluginDependencyResult getDependenciesToEnable(final List<Plugin> plugins, final Set<String> marked,
final DirectedGraph<String, DefaultEdge> graph, final PluginStatusResolver pluginStatusResolver) {
Set<PluginDependencyInformation> disabledDependencies = new HashSet<PluginDependencyInformation>();
Set<PluginDependencyInformation> unsatisfiedDependencies = new HashSet<PluginDependencyInformation>();
Set<String> argumentPluginIdentifiersSet = getArgumentIdentifiersSet(plugins);
for (Plugin plugin : plugins) {
if (marked.contains(plugin.getIdentifier())) {
continue;
}
marked.add(plugin.getIdentifier());
graph.addVertex(plugin.getIdentifier());
for (PluginDependencyInformation dependencyInfo : plugin.getRequiredPlugins()) {
Plugin requiredPlugin = pluginAccessor.getPlugin(dependencyInfo.getIdentifier());
if (requiredPlugin == null) {
if (!argumentPluginIdentifiersSet.contains(dependencyInfo.getIdentifier())) {
unsatisfiedDependencies.add(dependencyInfo);
}
continue;
}
graph.addVertex(dependencyInfo.getIdentifier());
graph.addEdge(plugin.getIdentifier(), dependencyInfo.getIdentifier());
if (!isPluginDisabled(requiredPlugin, pluginStatusResolver)) {
continue;
}
if (dependencyInfo.contains(requiredPlugin.getVersion())) {
disabledDependencies.add(dependencyInfo);
PluginDependencyResult nextLevelDependencioesResult = getDependenciesToEnable(
Collections.singletonList(requiredPlugin), marked, graph, pluginStatusResolver);
if (!nextLevelDependencioesResult.getUnsatisfiedDependencies().isEmpty()
|| nextLevelDependencioesResult.isCyclic()) {
return nextLevelDependencioesResult;
}
disabledDependencies.addAll(nextLevelDependencioesResult.getDependenciesToEnable());
} else {
unsatisfiedDependencies.add(dependencyInfo);
}
}
}
if (!unsatisfiedDependencies.isEmpty()) {
return PluginDependencyResultImpl.unsatisfiedDependencies(unsatisfiedDependencies);
}
if (disabledDependencies.isEmpty()) {
return PluginDependencyResultImpl.satisfiedDependencies();
}
Iterator<PluginDependencyInformation> dependencyInfoIterator = disabledDependencies.iterator();
while (dependencyInfoIterator.hasNext()) {
if (argumentPluginIdentifiersSet.contains(dependencyInfoIterator.next().getIdentifier())) {
dependencyInfoIterator.remove();
}
}
return PluginDependencyResultImpl.dependenciesToEnable(disabledDependencies);
}
@Override
public PluginDependencyResult getDependenciesToDisable(final List<Plugin> plugins,
final PluginStatusResolver pluginStatusResolver) {
return PluginDependencyResultImpl.dependenciesToDisable(getDependentPlugins(plugins, false, pluginStatusResolver));
}
@Override
public PluginDependencyResult getDependenciesToUninstall(final List<Plugin> plugins,
final PluginStatusResolver pluginStatusResolver) {
return PluginDependencyResultImpl.dependenciesToUninstall(getDependentPlugins(plugins, true, pluginStatusResolver));
}
@Override
public PluginDependencyResult getDependenciesToUpdate(final Plugin existingPlugin, final Plugin newPlugin,
final PluginStatusResolver pluginStatusResolver) {
Set<PluginDependencyInformation> dependentPlugins = getDependentPlugins(Collections.singletonList(existingPlugin), false,
pluginStatusResolver);
Set<PluginDependencyInformation> dependenciesToDisableUnsatisfiedAfterUpdate = new HashSet<PluginDependencyInformation>();
for (Plugin plugin : pluginAccessor.getPlugins()) {
if (pluginStatusResolver.isPluginNotInstalled(plugin)) {
continue;
}
for (PluginDependencyInformation dependencyInfo : plugin.getRequiredPlugins()) {
if (dependencyInfo.getIdentifier().equals(existingPlugin.getIdentifier())) {
if (!dependencyInfo.contains(newPlugin.getVersion())) {
dependenciesToDisableUnsatisfiedAfterUpdate.add(new PluginDependencyInformation(plugin.getIdentifier()));
}
break;
}
}
}
return PluginDependencyResultImpl.dependenciesToUpdate(dependentPlugins, dependenciesToDisableUnsatisfiedAfterUpdate);
}
@Override
public List<Plugin> sortPluginsInDependencyOrder(final Collection<Plugin> plugins, final Map<String, Plugin> allPlugins) {
// Map: not initialized pugins -> its dependencies
Map<String, Set<String>> notInitializedPlugins = createPluginsMapWithDependencies(plugins);
List<String> initializedPlugins = new LinkedList<String>();
// While not all plugins are initialized...
while (!notInitializedPlugins.isEmpty()) {
Iterator<Map.Entry<String, Set<String>>> pluginsToInitializedIt = notInitializedPlugins.entrySet().iterator();
// ..for all not initialized plugins...
while (pluginsToInitializedIt.hasNext()) {
Map.Entry<String, Set<String>> pluginToInitializeEntry = pluginsToInitializedIt.next();
// ...if plugin has no dependencies...
if (pluginToInitializeEntry.getValue().isEmpty()) {
// ...set this plugin as initialized...
initializedPlugins.add(pluginToInitializeEntry.getKey());
pluginsToInitializedIt.remove();
// ...and remove it from all dependencies
for (Set<String> dependencies : notInitializedPlugins.values()) {
dependencies.remove(pluginToInitializeEntry.getKey());
}
}
}
}
List<Plugin> sortedPlugins = convertIdentifiersToPlugins(initializedPlugins, allPlugins);
Collections.sort(sortedPlugins, new DependencyComparator());
return sortedPlugins;
}
@Override
public List<Plugin> sortPluginsInDependencyOrder(final Collection<Plugin> plugins) {
return sortPluginsInDependencyOrder(plugins, null);
}
private Set<PluginDependencyInformation> getDependentPlugins(final List<Plugin> plugins, final boolean includeDisabled,
final PluginStatusResolver pluginStatusResolver) {
List<Plugin> enabledDependencyPlugins = new LinkedList<Plugin>();
Set<String> argumentPluginIdentifiersSet = getArgumentIdentifiersSet(plugins);
for (Plugin plugin : pluginAccessor.getPlugins()) {
if (includeDisabled) {
if (pluginStatusResolver.isPluginNotInstalled(plugin)) {
continue;
}
} else {
if (!pluginStatusResolver.isPluginEnabled(plugin)) {
continue;
}
}
for (Plugin pluginToDisable : plugins) {
for (PluginDependencyInformation dependencyInfo : plugin.getRequiredPlugins()) {
if (dependencyInfo.getIdentifier().equals(pluginToDisable.getIdentifier())) {
enabledDependencyPlugins.add(plugin);
break;
}
}
}
}
Set<PluginDependencyInformation> enabledDependencies = new HashSet<PluginDependencyInformation>();
for (Plugin plugin : enabledDependencyPlugins) {
enabledDependencies.add(new PluginDependencyInformation(plugin.getIdentifier()));
}
if (!enabledDependencyPlugins.isEmpty()) {
PluginDependencyResult nextLevelDependencioesResult = getDependenciesToDisable(enabledDependencyPlugins,
pluginStatusResolver);
enabledDependencies.addAll(nextLevelDependencioesResult.getDependenciesToDisable());
}
Iterator<PluginDependencyInformation> dependencyInfoIterator = enabledDependencies.iterator();
while (dependencyInfoIterator.hasNext()) {
if (argumentPluginIdentifiersSet.contains(dependencyInfoIterator.next().getIdentifier())) {
dependencyInfoIterator.remove();
}
}
return enabledDependencies;
}
private Map<String, Set<String>> createPluginsMapWithDependencies(final Collection<Plugin> plugins) {
Map<String, Set<String>> resultMap = new HashMap<String, Set<String>>();
for (Plugin plugin : plugins) {
resultMap.put(plugin.getIdentifier(), null);
}
for (Plugin plugin : plugins) {
Set<String> dependencyIdentifiers = new HashSet<String>();
for (PluginDependencyInformation dependency : plugin.getRequiredPlugins()) {
if (resultMap.containsKey(dependency.getIdentifier())) {
dependencyIdentifiers.add(dependency.getIdentifier());
}
}
resultMap.put(plugin.getIdentifier(), dependencyIdentifiers);
}
return resultMap;
}
private List<Plugin> convertIdentifiersToPlugins(final Collection<String> pluginIdetifiers,
final Map<String, Plugin> allPlugins) {
List<Plugin> resultList = new LinkedList<Plugin>();
for (String pluginIdentifier : pluginIdetifiers) {
if (allPlugins == null) {
resultList.add(pluginAccessor.getPlugin(pluginIdentifier));
} else {
resultList.add(allPlugins.get(pluginIdentifier));
}
}
return resultList;
}
private Set<String> getArgumentIdentifiersSet(final List<Plugin> plugins) {
Set<String> argumentPluginInformationsSet = new HashSet<String>();
for (Plugin plugin : plugins) {
argumentPluginInformationsSet.add(plugin.getIdentifier());
}
return argumentPluginInformationsSet;
}
private boolean isPluginDisabled(final Plugin plugin, final PluginStatusResolver pluginStatusResolver) {
return pluginStatusResolver.isPluginDisabled(plugin) || pluginStatusResolver.isPluginNotInstalled(plugin);
}
void setPluginAccessor(final PluginAccessor pluginAccessor) {
this.pluginAccessor = pluginAccessor;
}
}