001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.util.graph.transformer;
020
021import java.util.Collection;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.IdentityHashMap;
026import java.util.Map;
027import java.util.Objects;
028import java.util.Set;
029
030import org.eclipse.aether.RepositoryException;
031import org.eclipse.aether.artifact.Artifact;
032import org.eclipse.aether.collection.DependencyGraphTransformationContext;
033import org.eclipse.aether.collection.DependencyGraphTransformer;
034import org.eclipse.aether.graph.Dependency;
035import org.eclipse.aether.graph.DependencyNode;
036
037import static java.util.Objects.requireNonNull;
038
039/**
040 * A dependency graph transformer that identifies conflicting dependencies. When this transformer has executed, the
041 * transformation context holds a {@code Map<DependencyNode, String>} where dependency nodes that belong to the same
042 * conflict group will have an equal conflict identifier. This map is stored using the key
043 * {@link TransformationContextKeys#CONFLICT_IDS}.
044 */
045public final class ConflictMarker implements DependencyGraphTransformer {
046
047    /**
048     * After the execution of this method, every DependencyNode with an attached dependency is member of one conflict
049     * group.
050     *
051     * @see DependencyGraphTransformer#transformGraph(DependencyNode, DependencyGraphTransformationContext)
052     */
053    @Override
054    public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransformationContext context)
055            throws RepositoryException {
056        requireNonNull(node, "node cannot be null");
057        requireNonNull(context, "context cannot be null");
058        @SuppressWarnings("unchecked")
059        Map<String, Object> stats = (Map<String, Object>) context.get(TransformationContextKeys.STATS);
060        long time1 = System.nanoTime();
061
062        Map<DependencyNode, Boolean> nodes = new IdentityHashMap<>(1024);
063        Map<Key, ConflictGroup> groups = new HashMap<>(1024);
064
065        analyze(node, nodes, groups, new int[] {0});
066
067        long time2 = System.nanoTime();
068
069        Map<DependencyNode, String> conflictIds = mark(nodes.keySet(), groups);
070
071        context.put(TransformationContextKeys.CONFLICT_IDS, conflictIds);
072
073        if (stats != null) {
074            long time3 = System.nanoTime();
075            stats.put("ConflictMarker.analyzeTime", time2 - time1);
076            stats.put("ConflictMarker.markTime", time3 - time2);
077            stats.put("ConflictMarker.nodeCount", nodes.size());
078        }
079
080        return node;
081    }
082
083    private void analyze(
084            DependencyNode node, Map<DependencyNode, Boolean> nodes, Map<Key, ConflictGroup> groups, int[] counter) {
085        if (nodes.put(node, Boolean.TRUE) != null) {
086            return;
087        }
088
089        Set<Key> keys = getKeys(node);
090        if (!keys.isEmpty()) {
091            ConflictGroup group = null;
092            boolean fixMappings = false;
093
094            for (Key key : keys) {
095                ConflictGroup g = groups.get(key);
096
097                if (group != g) {
098                    if (group == null) {
099                        Set<Key> newKeys = merge(g.keys, keys);
100                        if (newKeys == g.keys) {
101                            group = g;
102                            break;
103                        } else {
104                            group = new ConflictGroup(newKeys, counter[0]++);
105                            fixMappings = true;
106                        }
107                    } else if (g == null) {
108                        fixMappings = true;
109                    } else {
110                        Set<Key> newKeys = merge(g.keys, group.keys);
111                        if (newKeys == g.keys) {
112                            group = g;
113                            fixMappings = false;
114                            break;
115                        } else if (newKeys != group.keys) {
116                            group = new ConflictGroup(newKeys, counter[0]++);
117                            fixMappings = true;
118                        }
119                    }
120                }
121            }
122
123            if (group == null) {
124                group = new ConflictGroup(keys, counter[0]++);
125                fixMappings = true;
126            }
127            if (fixMappings) {
128                for (Key key : group.keys) {
129                    groups.put(key, group);
130                }
131            }
132        }
133
134        for (DependencyNode child : node.getChildren()) {
135            analyze(child, nodes, groups, counter);
136        }
137    }
138
139    private Set<Key> merge(Set<Key> keys1, Set<Key> keys2) {
140        int size1 = keys1.size();
141        int size2 = keys2.size();
142
143        if (size1 < size2) {
144            if (keys2.containsAll(keys1)) {
145                return keys2;
146            }
147        } else {
148            if (keys1.containsAll(keys2)) {
149                return keys1;
150            }
151        }
152
153        Set<Key> keys = new HashSet<>();
154        keys.addAll(keys1);
155        keys.addAll(keys2);
156        return keys;
157    }
158
159    private Set<Key> getKeys(DependencyNode node) {
160        Set<Key> keys;
161
162        Dependency dependency = node.getDependency();
163
164        if (dependency == null) {
165            keys = Collections.emptySet();
166        } else {
167            Key key = toKey(dependency.getArtifact());
168
169            if (node.getRelocations().isEmpty() && node.getAliases().isEmpty()) {
170                keys = Collections.singleton(key);
171            } else {
172                keys = new HashSet<>();
173                keys.add(key);
174
175                for (Artifact relocation : node.getRelocations()) {
176                    key = toKey(relocation);
177                    keys.add(key);
178                }
179
180                for (Artifact alias : node.getAliases()) {
181                    key = toKey(alias);
182                    keys.add(key);
183                }
184            }
185        }
186
187        return keys;
188    }
189
190    private Map<DependencyNode, String> mark(Collection<DependencyNode> nodes, Map<Key, ConflictGroup> groups) {
191        Map<DependencyNode, String> conflictIds = new IdentityHashMap<>(nodes.size() + 1);
192
193        for (DependencyNode node : nodes) {
194            Dependency dependency = node.getDependency();
195            if (dependency != null) {
196                Key key = toKey(dependency.getArtifact());
197                conflictIds.put(
198                        node, String.valueOf(groups.get(key).index).intern()); // interning it as is expected so in UT
199            }
200        }
201
202        return conflictIds;
203    }
204
205    private static Key toKey(Artifact artifact) {
206        return new Key(artifact);
207    }
208
209    static class ConflictGroup {
210
211        final Set<Key> keys;
212
213        final int index;
214
215        ConflictGroup(Set<Key> keys, int index) {
216            this.keys = keys;
217            this.index = index;
218        }
219
220        @Override
221        public String toString() {
222            return String.valueOf(keys);
223        }
224    }
225
226    static class Key {
227
228        private final Artifact artifact;
229        private final int hashCode;
230
231        Key(Artifact artifact) {
232            this.artifact = artifact;
233            this.hashCode = Objects.hash(
234                    artifact.getArtifactId(), artifact.getGroupId(), artifact.getExtension(), artifact.getClassifier());
235        }
236
237        @Override
238        public boolean equals(Object obj) {
239            if (obj == this) {
240                return true;
241            } else if (!(obj instanceof Key)) {
242                return false;
243            }
244            Key that = (Key) obj;
245            return artifact.getArtifactId().equals(that.artifact.getArtifactId())
246                    && artifact.getGroupId().equals(that.artifact.getGroupId())
247                    && artifact.getExtension().equals(that.artifact.getExtension())
248                    && artifact.getClassifier().equals(that.artifact.getClassifier());
249        }
250
251        @Override
252        public int hashCode() {
253            return hashCode;
254        }
255
256        @Override
257        public String toString() {
258            return artifact.getGroupId()
259                    + ':'
260                    + artifact.getArtifactId()
261                    + ':'
262                    + artifact.getClassifier()
263                    + ':'
264                    + artifact.getExtension();
265        }
266    }
267}