package org.jboss.windup.config.loader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jboss.forge.furnace.proxy.Proxies;
import org.jboss.windup.config.AbstractRuleProvider;
import org.jboss.windup.config.RuleProvider;
import org.jboss.windup.config.phase.RulePhase;
import org.jboss.windup.util.exception.WindupMultiStringException;
import org.jgrapht.alg.CycleDetector;
import org.jgrapht.graph.DefaultDirectedWeightedGraph;
import org.jgrapht.graph.DefaultEdge;
import org.jgrapht.traverse.TopologicalOrderIterator;
/**
* Sorts {@link RuleProvider}s based upon their executeBefore/executeAfter methods.
*
* @author <a href="mailto:jesse.sightler@gmail.com">Jesse Sightler</a>
* @author <a href="mailto:ozizka@redhat.com">Ondrej Zizka</a>
*/
public class RuleProviderSorter
{
/**
* All {@link RuleProvider}s
*/
private List<RuleProvider> providers;
/**
* Maps from the RuleProvider class back to the instance of RuleProvider
*/
private final IdentityHashMap<Class<? extends RuleProvider>, RuleProvider> classToProviderMap = new IdentityHashMap<>();
/**
* Maps from the provider's ID to the RuleProvider
*/
private final Map<String, RuleProvider> idToProviderMap = new HashMap<>();
private RuleProviderSorter(List<RuleProvider> providers)
{
this.providers = new ArrayList<>(providers);
initializeLookupCaches();
sort();
}
/**
* Sort the provided list of {@link RuleProvider}s and return the result.
*/
public static List<RuleProvider> sort(List<RuleProvider> providers)
{
RuleProviderSorter sorter = new RuleProviderSorter(providers);
return sorter.getProviders();
}
/**
* Gets the provider list
*/
private List<RuleProvider> getProviders()
{
return providers;
}
/**
* Initializes lookup caches that are used during sort to lookup providers by ID or Java {@link Class}.
*/
private void initializeLookupCaches()
{
for (RuleProvider provider : providers)
{
Class<? extends RuleProvider> unproxiedClass = unwrapType(provider.getClass());
classToProviderMap.put(unproxiedClass, provider);
idToProviderMap.put(provider.getMetadata().getID(), provider);
}
}
/**
* Perform the entire sort operation
*/
private void sort()
{
DefaultDirectedWeightedGraph<RuleProvider, DefaultEdge> graph = new DefaultDirectedWeightedGraph<>(
DefaultEdge.class);
for (RuleProvider provider : providers)
{
graph.addVertex(provider);
}
addProviderRelationships(graph);
checkForCycles(graph);
List<RuleProvider> result = new ArrayList<>(this.providers.size());
TopologicalOrderIterator<RuleProvider, DefaultEdge> iterator = new TopologicalOrderIterator<>(graph);
while (iterator.hasNext())
{
RuleProvider provider = iterator.next();
result.add(provider);
}
this.providers = Collections.unmodifiableList(result);
int index = 0;
for (RuleProvider provider : this.providers)
{
if (provider instanceof AbstractRuleProvider)
((AbstractRuleProvider) provider).setExecutionIndex(index++);
}
}
/**
* Add edges between {@link WinduPRuleProvider}s based upon their dependency relationships.
*/
private void addProviderRelationships(DefaultDirectedWeightedGraph<RuleProvider, DefaultEdge> graph)
{
linkRulePhases();
for (RuleProvider provider : providers)
{
RuleProvider phaseProvider = getByClass(provider.getMetadata().getPhase());
List<String> errors = new LinkedList<>();
for (Class<? extends RuleProvider> clz : provider.getMetadata().getExecuteAfter())
{
// add connections to ruleproviders that should execute before this one
addExecuteAfterRelationship(graph, provider, errors, clz);
}
if (phaseProvider != null)
{
if (provider.getMetadata().getPhase() != Proxies.unwrap(provider).getClass())
addExecuteAfterRelationship(graph, provider, errors, provider.getMetadata().getPhase());
for (Class<? extends RuleProvider> clz : phaseProvider.getMetadata().getExecuteAfter())
{
addExecuteAfterRelationship(graph, provider, errors, clz);
}
}
for (Class<? extends RuleProvider> clz : provider.getMetadata().getExecuteBefore())
{
// add connections to ruleproviders that should execute after this one
addExecuteBeforeRelationship(graph, provider, errors, clz);
}
if (phaseProvider != null)
{
for (Class<? extends RuleProvider> clz : phaseProvider.getMetadata().getExecuteBefore())
{
addExecuteBeforeRelationship(graph, provider, errors, clz);
}
}
for (String depID : provider.getMetadata().getExecuteAfterIDs())
{
// add connections to ruleproviders that should execute before this one (by String ID)
RuleProvider otherProvider = getByID(depID);
if (otherProvider == null)
{
errors.add("RuleProvider " + provider.getMetadata().getID() + " is specified to execute after: "
+ depID + " but this provider could not be found.");
}
else
graph.addEdge(otherProvider, provider);
}
for (String depID : provider.getMetadata().getExecuteBeforeIDs())
{
// add connections to ruleproviders that should execute before this one (by String ID)
RuleProvider otherProvider = getByID(depID);
if (otherProvider == null)
{
errors.add("RuleProvider " + provider.getMetadata().getID() + " is specified to execute before: "
+ depID + " but this provider could not be found.");
}
else
graph.addEdge(provider, otherProvider);
}
if (!errors.isEmpty())
throw new WindupMultiStringException("Some rules to be executed before or after were not found:", errors);
}
}
@SuppressWarnings("unchecked")
private void linkRulePhases()
{
// Go through all of the RulePhases and link tail-to-head-to-tail
// Basically this just makes sure that each phase has both an executeBefore and an executeAfter, linked
// to the next phase in the execution cycle. This isn't strictly necessary, but it does make debugging easier,
// and makes the execution order more predictable.
for (RuleProvider provider : providers)
{
if (provider instanceof RulePhase)
{
if (provider.getMetadata().getExecuteBefore().isEmpty())
{
// find any that should execute after this one
for (RuleProvider otherProvider : providers)
{
if (otherProvider instanceof RulePhase)
{
if (otherProvider.getMetadata().getExecuteAfter().contains(Proxies.unwrap(provider).getClass()))
{
((RulePhase) provider).setExecuteBefore((Class<? extends RulePhase>) Proxies.unwrap((RulePhase) otherProvider)
.getClass());
}
}
}
}
if (provider.getMetadata().getExecuteAfter().isEmpty())
{
// find any that should execute before this one
for (RuleProvider otherProvider : providers)
{
if (otherProvider instanceof RulePhase)
{
if (otherProvider.getMetadata().getExecuteBefore().contains(Proxies.unwrap(provider).getClass()))
{
((RulePhase) provider).setExecuteAfter((Class<? extends RulePhase>) Proxies.unwrap((RulePhase) otherProvider)
.getClass());
}
}
}
}
}
}
}
private void addExecuteBeforeRelationship(DefaultDirectedWeightedGraph<RuleProvider, DefaultEdge> graph, RuleProvider provider,
List<String> errors, Class<? extends RuleProvider> clz)
{
RuleProvider otherProvider = getByClass(clz);
if (otherProvider == null)
{
errors.add("RuleProvider " + provider.getMetadata().getID() + " is specified to execute before: "
+ clz.getName() + " but this class could not be found.");
}
else
graph.addEdge(provider, otherProvider);
}
private void addExecuteAfterRelationship(DefaultDirectedWeightedGraph<RuleProvider, DefaultEdge> graph, RuleProvider provider,
List<String> errors, Class<? extends RuleProvider> clz)
{
RuleProvider otherProvider = getByClass(clz);
if (otherProvider == null)
{
errors.add("RuleProvider " + provider.getMetadata().getID() + " is specified to execute after class: "
+ clz.getName() + " but this class could not be found.");
}
else
graph.addEdge(otherProvider, provider);
}
/**
* Use the jgrapht cycle checker to detect any cycles in the provided dependency graph.
*/
private void checkForCycles(DefaultDirectedWeightedGraph<RuleProvider, DefaultEdge> graph)
{
CycleDetector<RuleProvider, DefaultEdge> cycleDetector = new CycleDetector<>(graph);
if (cycleDetector.detectCycles())
{
// if we have cycles, then try to throw an exception with some usable data
Set<RuleProvider> cycles = cycleDetector.findCycles();
StringBuilder errorSB = new StringBuilder();
for (RuleProvider cycle : cycles)
{
errorSB.append("Found dependency cycle involving: " + cycle.getMetadata().getID() + "\n");
Set<RuleProvider> subCycleSet = cycleDetector.findCyclesContainingVertex(cycle);
for (RuleProvider subCycle : subCycleSet)
{
errorSB.append("\tSubcycle: " + subCycle.getMetadata().getID() + "\n");
}
}
throw new RuntimeException("Dependency cycles detected: " + errorSB.toString());
}
}
private RuleProvider getByClass(Class<? extends RuleProvider> c)
{
return classToProviderMap.get(c);
}
private RuleProvider getByID(String id)
{
return idToProviderMap.get(id);
}
@SuppressWarnings("unchecked")
private <T> Class<T> unwrapType(Class<T> wrapped)
{
return (Class<T>) Proxies.unwrapProxyTypes(wrapped);
}
}