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.ArrayList; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.IdentityHashMap; 027import java.util.LinkedHashMap; 028import java.util.List; 029import java.util.Map; 030 031import org.eclipse.aether.RepositoryException; 032import org.eclipse.aether.collection.DependencyGraphTransformationContext; 033import org.eclipse.aether.collection.DependencyGraphTransformer; 034import org.eclipse.aether.graph.DependencyNode; 035 036import static java.util.Objects.requireNonNull; 037 038/** 039 * A dependency graph transformer that creates a topological sorting of the conflict ids which have been assigned to the 040 * dependency nodes. Conflict ids are sorted according to the dependency relation induced by the dependency graph. This 041 * transformer will query the key {@link TransformationContextKeys#CONFLICT_IDS} in the transformation context for an 042 * existing mapping of nodes to their conflicts ids. In absence of this map, the transformer will automatically invoke 043 * the {@link ConflictMarker} to calculate the conflict ids. When this transformer has executed, the transformation 044 * context holds a {@code List<String>} that denotes the topologically sorted conflict ids. The list will be stored 045 * using the key {@link TransformationContextKeys#SORTED_CONFLICT_IDS}. In addition, the transformer will store a 046 * {@code Collection<Collection<String>>} using the key {@link TransformationContextKeys#CYCLIC_CONFLICT_IDS} that 047 * describes cycles among conflict ids. 048 */ 049public final class ConflictIdSorter implements DependencyGraphTransformer { 050 051 @SuppressWarnings("unchecked") 052 @Override 053 public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransformationContext context) 054 throws RepositoryException { 055 requireNonNull(node, "node cannot be null"); 056 requireNonNull(context, "context cannot be null"); 057 Map<DependencyNode, String> conflictIds = 058 (Map<DependencyNode, String>) context.get(TransformationContextKeys.CONFLICT_IDS); 059 if (conflictIds == null) { 060 ConflictMarker marker = new ConflictMarker(); 061 marker.transformGraph(node, context); 062 063 conflictIds = (Map<DependencyNode, String>) context.get(TransformationContextKeys.CONFLICT_IDS); 064 } 065 066 @SuppressWarnings("unchecked") 067 Map<String, Object> stats = (Map<String, Object>) context.get(TransformationContextKeys.STATS); 068 long time1 = System.nanoTime(); 069 070 Map<String, ConflictId> ids = new LinkedHashMap<>(256); 071 072 ConflictId id = null; 073 String key = conflictIds.get(node); 074 if (key != null) { 075 id = new ConflictId(key, 0); 076 ids.put(key, id); 077 } 078 079 Map<DependencyNode, Boolean> visited = new IdentityHashMap<>(conflictIds.size()); 080 081 buildConflictIdDAG(ids, node, id, 0, visited, conflictIds); 082 083 long time2 = System.nanoTime(); 084 085 int cycles = topoSortConflictIds(ids.values(), context); 086 087 if (stats != null) { 088 long time3 = System.nanoTime(); 089 stats.put("ConflictIdSorter.graphTime", time2 - time1); 090 stats.put("ConflictIdSorter.topsortTime", time3 - time2); 091 stats.put("ConflictIdSorter.conflictIdCount", ids.size()); 092 stats.put("ConflictIdSorter.conflictIdCycleCount", cycles); 093 } 094 095 return node; 096 } 097 098 private void buildConflictIdDAG( 099 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}