All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.takari.maven.builder.smart.DependencyGraph Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2014-2024 Takari, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache Software License v2.0
 * which accompanies this distribution, and is available at
 * https://www.apache.org/licenses/LICENSE-2.0
 */
package io.takari.maven.builder.smart;

import java.io.IOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.execution.ProjectDependencyGraph;
import org.apache.maven.project.MavenProject;

public class DependencyGraph {

    private final List projects;
    private final Map> upstreams;
    private final Map> transitiveUpstreams;
    private final Map> downstreams;

    @SuppressWarnings("unchecked")
    public static DependencyGraph fromMaven(MavenSession session) {
        Map data = session.getRequest().getData();
        DependencyGraph graph = (DependencyGraph) data.get(DependencyGraph.class.getName());
        if (graph == null) {
            graph = fromMaven(session.getProjectDependencyGraph());
            data.put(DependencyGraph.class.getName(), graph);
        }
        return graph;
    }

    static DependencyGraph fromMaven(ProjectDependencyGraph graph) {
        final List projects = graph.getSortedProjects();
        Map> upstreams =
                projects.stream().collect(Collectors.toMap(p -> p, p -> graph.getUpstreamProjects(p, false)));
        Map> downstreams =
                projects.stream().collect(Collectors.toMap(p -> p, p -> graph.getDownstreamProjects(p, false)));
        return new DependencyGraph<>(Collections.unmodifiableList(projects), upstreams, downstreams);
    }

    public DependencyGraph(List projects, Map> upstreams, Map> downstreams) {
        this.projects = projects;
        this.upstreams = upstreams;
        this.downstreams = downstreams;

        this.transitiveUpstreams = new HashMap<>();
        projects.forEach(this::transitiveUpstreams); // topological ordering of projects matters
    }

    DependencyGraph(
            List projects,
            Map> upstreams,
            Map> downstreams,
            Map> transitiveUpstreams) {
        this.projects = projects;
        this.upstreams = upstreams;
        this.downstreams = downstreams;
        this.transitiveUpstreams = transitiveUpstreams;
    }

    public Stream getDownstreamProjects(K project) {
        return downstreams.get(project).stream();
    }

    public Stream getUpstreamProjects(K project) {
        return upstreams.get(project).stream();
    }

    public boolean isRoot(K project) {
        return upstreams.get(project).isEmpty();
    }

    public Stream getProjects() {
        return projects.stream();
    }

    public int computeMaxWidth(int max, long maxTimeMillis) {
        return new DagWidth<>(this).getMaxWidth(max, maxTimeMillis);
    }

    public void store(Function toString, Path path) {
        try (Writer w = Files.newBufferedWriter(path)) {
            store(toString, w);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public void store(Function toString, Appendable w) {
        getProjects().forEach(k -> {
            try {
                w.append(toString.apply(k));
                w.append(" = ");
                w.append(getUpstreamProjects(k).map(toString).collect(Collectors.joining(",")));
                w.append(System.lineSeparator());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        store(k -> k.toString(), sb);
        return sb.toString();
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((downstreams == null) ? 0 : downstreams.hashCode());
        result = prime * result + ((projects == null) ? 0 : projects.hashCode());
        result = prime * result + ((upstreams == null) ? 0 : upstreams.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        @SuppressWarnings("unchecked")
        DependencyGraph other = (DependencyGraph) obj;
        if (downstreams == null) {
            if (other.downstreams != null) return false;
        } else if (!downstreams.equals(other.downstreams)) return false;
        if (projects == null) {
            if (other.projects != null) return false;
        } else if (!projects.equals(other.projects)) return false;
        if (upstreams == null) {
            if (other.upstreams != null) return false;
        } else if (!upstreams.equals(other.upstreams)) return false;
        return true;
    }

    /**
     * Creates a new {@link DependencyGraph} which is a 
     * transitive reduction of this {@link DependencyGraph}. The reduction operation keeps the set of graph nodes
     * unchanged and it reduces the set of edges in the following way: An edge {@code C -> A} is removed if an edge
     * {@code C -> B} exists such that {@code A != B} and the set of nodes reachable from {@code B} contains {@code A};
     * otherwise the edge {@code C -> A} is kept in the reduced graph.
     * 

* Examples: * *

     * Original     Reduced
     *
     *    A           A
     *   /|          /
     *  B |         B
     *   \|          \
     *    C           C
     *
     *
     *    A           A
     *   /|\         /
     *  B | |       B
     *   \| |        \
     *    C |         C
     *     \|          \
     *      D           D
     *
     * 
* * * @return a transitive reduction of this {@link DependencyGraph} */ DependencyGraph reduce() { final Map> newUpstreams = new HashMap<>(); final Map> newDownstreams = new HashMap<>(); for (K node : projects) { final List oldNodeUpstreams = upstreams.get(node); final List newNodeUpstreams; newDownstreams.computeIfAbsent(node, k -> new ArrayList<>()); if (oldNodeUpstreams.size() == 0) { newNodeUpstreams = new ArrayList<>(oldNodeUpstreams); } else if (oldNodeUpstreams.size() == 1) { newNodeUpstreams = new ArrayList<>(oldNodeUpstreams); newDownstreams .computeIfAbsent(newNodeUpstreams.get(0), k -> new ArrayList<>()) .add(node); } else { newNodeUpstreams = new ArrayList<>(oldNodeUpstreams.size()); for (K leftNode : oldNodeUpstreams) { if (oldNodeUpstreams.stream() .filter(rightNode -> leftNode != rightNode) .noneMatch(rightNode -> transitiveUpstreams.get(rightNode).contains(leftNode))) { newNodeUpstreams.add(leftNode); newDownstreams .computeIfAbsent(leftNode, k -> new ArrayList<>()) .add(node); } } } newUpstreams.put(node, newNodeUpstreams); } return new DependencyGraph(projects, newUpstreams, newDownstreams, transitiveUpstreams); } /** * Compute the set of nodes reachable from the given {@code node} through the {@code is upstream of} relation. The * {@code node} itself is not a part of the returned set. * * @param node the node for which the transitive upstream should be computed * @return the set of transitive upstreams */ Set transitiveUpstreams(K node) { Set result = transitiveUpstreams.get(node); if (result == null) { final List firstOrderUpstreams = this.upstreams.get(node); result = new HashSet<>(firstOrderUpstreams); firstOrderUpstreams.stream().map(this::transitiveUpstreams).forEach(result::addAll); transitiveUpstreams.put(node, result); } return result; } static class DagWidth { private final DependencyGraph graph; public DagWidth(DependencyGraph graph) { this.graph = graph.reduce(); } public int getMaxWidth() { return getMaxWidth(Integer.MAX_VALUE); } public int getMaxWidth(int maxmax) { return getMaxWidth(maxmax, Long.MAX_VALUE); } public int getMaxWidth(int maxmax, long maxTimeMillis) { int max = 0; if (maxmax < graph.transitiveUpstreams.size()) { // try inverted upstream bound Map, Set> mapByUpstreams = new HashMap<>(); graph.transitiveUpstreams.forEach((k, ups) -> { mapByUpstreams.computeIfAbsent(ups, n -> new HashSet<>()).add(k); }); max = mapByUpstreams.values().stream().mapToInt(Set::size).max().orElse(0); if (max >= maxmax) { return maxmax; } } long tmax = System.currentTimeMillis() + maxTimeMillis; int tries = 0; SubsetIterator iterator = new SubsetIterator(getRoots()); while (max < maxmax && iterator.hasNext()) { if (++tries % 100 == 0 && System.currentTimeMillis() < tmax) { return maxmax; } List l = iterator.next(); max = Math.max(max, l.size()); } return Math.min(max, maxmax); } private class SubsetIterator implements Iterator> { final List> nexts = new ArrayList<>(); final Set> visited = new HashSet<>(); public SubsetIterator(List roots) { nexts.add(roots); } @Override public boolean hasNext() { return !nexts.isEmpty(); } @Override public List next() { List list = nexts.remove(0); list.stream() .map(node -> ensembleWithChildrenOf(list, node)) .filter(visited::add) .forEach(nexts::add); return list; } } private List getRoots() { return graph.getProjects().filter(graph::isRoot).collect(Collectors.toList()); } List ensembleWithChildrenOf(List list, K node) { final List result = Stream.concat( list.stream().filter(k -> !Objects.equals(k, node)), graph.getDownstreamProjects(node).filter(k -> graph.transitiveUpstreams.get(k).stream() .noneMatch(k2 -> !Objects.equals(k2, node) && list.contains(k2)))) .distinct() .collect(Collectors.toList()); return result; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy