View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.util.graph.transformer;
20  
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.IdentityHashMap;
27  import java.util.LinkedHashMap;
28  import java.util.List;
29  import java.util.Map;
30  
31  import org.eclipse.aether.RepositoryException;
32  import org.eclipse.aether.collection.DependencyGraphTransformationContext;
33  import org.eclipse.aether.collection.DependencyGraphTransformer;
34  import org.eclipse.aether.graph.DependencyNode;
35  
36  import static java.util.Objects.requireNonNull;
37  
38  /**
39   * A dependency graph transformer that creates a topological sorting of the conflict ids which have been assigned to the
40   * dependency nodes. Conflict ids are sorted according to the dependency relation induced by the dependency graph. This
41   * transformer will query the key {@link TransformationContextKeys#CONFLICT_IDS} in the transformation context for an
42   * existing mapping of nodes to their conflicts ids. In absence of this map, the transformer will automatically invoke
43   * the {@link ConflictMarker} to calculate the conflict ids. When this transformer has executed, the transformation
44   * context holds a {@code List<String>} that denotes the topologically sorted conflict ids. The list will be stored
45   * using the key {@link TransformationContextKeys#SORTED_CONFLICT_IDS}. In addition, the transformer will store a
46   * {@code Collection<Collection<String>>} using the key {@link TransformationContextKeys#CYCLIC_CONFLICT_IDS} that
47   * describes cycles among conflict ids.
48   */
49  public final class ConflictIdSorter implements DependencyGraphTransformer {
50  
51      @SuppressWarnings("unchecked")
52      @Override
53      public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransformationContext context)
54              throws RepositoryException {
55          requireNonNull(node, "node cannot be null");
56          requireNonNull(context, "context cannot be null");
57          Map<DependencyNode, String> conflictIds =
58                  (Map<DependencyNode, String>) context.get(TransformationContextKeys.CONFLICT_IDS);
59          if (conflictIds == null) {
60              ConflictMarker marker = new ConflictMarker();
61              marker.transformGraph(node, context);
62  
63              conflictIds = (Map<DependencyNode, String>) context.get(TransformationContextKeys.CONFLICT_IDS);
64          }
65  
66          @SuppressWarnings("unchecked")
67          Map<String, Object> stats = (Map<String, Object>) context.get(TransformationContextKeys.STATS);
68          long time1 = System.nanoTime();
69  
70          Map<String, ConflictId> ids = new LinkedHashMap<>(256);
71  
72          ConflictId id = null;
73          String key = conflictIds.get(node);
74          if (key != null) {
75              id = new ConflictId(key, 0);
76              ids.put(key, id);
77          }
78  
79          Map<DependencyNode, Boolean> visited = new IdentityHashMap<>(conflictIds.size());
80  
81          buildConflictIdDAG(ids, node, id, 0, visited, conflictIds);
82  
83          long time2 = System.nanoTime();
84  
85          int cycles = topoSortConflictIds(ids.values(), context);
86  
87          if (stats != null) {
88              long time3 = System.nanoTime();
89              stats.put("ConflictIdSorter.graphTime", time2 - time1);
90              stats.put("ConflictIdSorter.topsortTime", time3 - time2);
91              stats.put("ConflictIdSorter.conflictIdCount", ids.size());
92              stats.put("ConflictIdSorter.conflictIdCycleCount", cycles);
93          }
94  
95          return node;
96      }
97  
98      private void buildConflictIdDAG(
99              Map<String, ConflictId> ids,
100             DependencyNode node,
101             ConflictId id,
102             int depth,
103             Map<DependencyNode, Boolean> visited,
104             Map<DependencyNode, String> conflictIds) {
105         if (visited.put(node, Boolean.TRUE) != null) {
106             return;
107         }
108 
109         depth++;
110 
111         for (DependencyNode child : node.getChildren()) {
112             String key = conflictIds.get(child);
113             ConflictId childId = ids.get(key);
114             if (childId == null) {
115                 childId = new ConflictId(key, depth);
116                 ids.put(key, childId);
117             } else {
118                 childId.pullup(depth);
119             }
120 
121             if (id != null) {
122                 id.add(childId);
123             }
124 
125             buildConflictIdDAG(ids, child, childId, depth, visited, conflictIds);
126         }
127     }
128 
129     private int topoSortConflictIds(Collection<ConflictId> conflictIds, DependencyGraphTransformationContext context) {
130         List<String> sorted = new ArrayList<>(conflictIds.size());
131 
132         RootQueue roots = new RootQueue(conflictIds.size() / 2);
133         for (ConflictId id : conflictIds) {
134             if (id.inDegree <= 0) {
135                 roots.add(id);
136             }
137         }
138 
139         processRoots(sorted, roots);
140 
141         boolean cycle = sorted.size() < conflictIds.size();
142 
143         while (sorted.size() < conflictIds.size()) {
144             // cycle -> deal gracefully with nodes still having positive in-degree
145 
146             ConflictId nearest = null;
147             for (ConflictId id : conflictIds) {
148                 if (id.inDegree <= 0) {
149                     continue;
150                 }
151                 if (nearest == null
152                         || id.minDepth < nearest.minDepth
153                         || (id.minDepth == nearest.minDepth && id.inDegree < nearest.inDegree)) {
154                     nearest = id;
155                 }
156             }
157 
158             nearest.inDegree = 0;
159             roots.add(nearest);
160 
161             processRoots(sorted, roots);
162         }
163 
164         Collection<Collection<String>> cycles = Collections.emptySet();
165         if (cycle) {
166             cycles = findCycles(conflictIds);
167         }
168 
169         context.put(TransformationContextKeys.SORTED_CONFLICT_IDS, sorted);
170         context.put(TransformationContextKeys.CYCLIC_CONFLICT_IDS, cycles);
171 
172         return cycles.size();
173     }
174 
175     private void processRoots(List<String> sorted, RootQueue roots) {
176         while (!roots.isEmpty()) {
177             ConflictId root = roots.remove();
178 
179             sorted.add(root.key);
180 
181             for (ConflictId child : root.children) {
182                 child.inDegree--;
183                 if (child.inDegree == 0) {
184                     roots.add(child);
185                 }
186             }
187         }
188     }
189 
190     private Collection<Collection<String>> findCycles(Collection<ConflictId> conflictIds) {
191         Collection<Collection<String>> cycles = new HashSet<>();
192 
193         Map<String, Integer> stack = new HashMap<>(128);
194         Map<ConflictId, Boolean> visited = new IdentityHashMap<>(conflictIds.size());
195         for (ConflictId id : conflictIds) {
196             findCycles(id, visited, stack, cycles);
197         }
198 
199         return cycles;
200     }
201 
202     private void findCycles(
203             ConflictId id,
204             Map<ConflictId, Boolean> visited,
205             Map<String, Integer> stack,
206             Collection<Collection<String>> cycles) {
207         Integer depth = stack.put(id.key, stack.size());
208         if (depth != null) {
209             stack.put(id.key, depth);
210             Collection<String> cycle = new HashSet<>();
211             for (Map.Entry<String, Integer> entry : stack.entrySet()) {
212                 if (entry.getValue() >= depth) {
213                     cycle.add(entry.getKey());
214                 }
215             }
216             cycles.add(cycle);
217         } else {
218             if (visited.put(id, Boolean.TRUE) == null) {
219                 for (ConflictId childId : id.children) {
220                     findCycles(childId, visited, stack, cycles);
221                 }
222             }
223             stack.remove(id.key);
224         }
225     }
226 
227     static final class ConflictId {
228 
229         final String key;
230 
231         Collection<ConflictId> children = Collections.emptySet();
232 
233         int inDegree;
234 
235         int minDepth;
236 
237         ConflictId(String key, int depth) {
238             this.key = key;
239             this.minDepth = depth;
240         }
241 
242         public void add(ConflictId child) {
243             if (children.isEmpty()) {
244                 children = new HashSet<>();
245             }
246             if (children.add(child)) {
247                 child.inDegree++;
248             }
249         }
250 
251         public void pullup(int depth) {
252             if (depth < minDepth) {
253                 minDepth = depth;
254                 depth++;
255                 for (ConflictId child : children) {
256                     child.pullup(depth);
257                 }
258             }
259         }
260 
261         @Override
262         public boolean equals(Object obj) {
263             if (this == obj) {
264                 return true;
265             } else if (!(obj instanceof ConflictId)) {
266                 return false;
267             }
268             ConflictId that = (ConflictId) obj;
269             return this.key.equals(that.key);
270         }
271 
272         @Override
273         public int hashCode() {
274             return key.hashCode();
275         }
276 
277         @Override
278         public String toString() {
279             return key + " @ " + minDepth + " <" + inDegree;
280         }
281     }
282 
283     static final class RootQueue {
284 
285         private int nextOut;
286 
287         private int nextIn;
288 
289         private ConflictId[] ids;
290 
291         RootQueue(int capacity) {
292             ids = new ConflictId[capacity + 16];
293         }
294 
295         boolean isEmpty() {
296             return nextOut >= nextIn;
297         }
298 
299         void add(ConflictId id) {
300             if (nextOut >= nextIn && nextOut > 0) {
301                 nextIn -= nextOut;
302                 nextOut = 0;
303             }
304             if (nextIn >= ids.length) {
305                 ConflictId[] tmp = new ConflictId[ids.length + ids.length / 2 + 16];
306                 System.arraycopy(ids, nextOut, tmp, 0, nextIn - nextOut);
307                 ids = tmp;
308                 nextIn -= nextOut;
309                 nextOut = 0;
310             }
311             int i;
312             for (i = nextIn - 1; i >= nextOut && id.minDepth < ids[i].minDepth; i--) {
313                 ids[i + 1] = ids[i];
314             }
315             ids[i + 1] = id;
316             nextIn++;
317         }
318 
319         ConflictId remove() {
320             return ids[nextOut++];
321         }
322     }
323 }