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.internal.test.util; 020 021import java.io.BufferedReader; 022import java.io.IOException; 023import java.io.InputStreamReader; 024import java.io.StringReader; 025import java.net.URL; 026import java.nio.charset.StandardCharsets; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collection; 030import java.util.Collections; 031import java.util.HashMap; 032import java.util.Iterator; 033import java.util.LinkedList; 034import java.util.List; 035import java.util.Map; 036 037import org.eclipse.aether.artifact.Artifact; 038import org.eclipse.aether.artifact.DefaultArtifact; 039import org.eclipse.aether.graph.DefaultDependencyNode; 040import org.eclipse.aether.graph.Dependency; 041import org.eclipse.aether.graph.DependencyNode; 042import org.eclipse.aether.version.InvalidVersionSpecificationException; 043import org.eclipse.aether.version.VersionScheme; 044 045/** 046 * Creates a dependency graph from a text description. <h2>Definition</h2> Each (non-empty) line in the input defines 047 * one node of the resulting graph: 048 * 049 * <pre> 050 * line ::= (indent? ("(null)" | node | reference))? comment? 051 * comment ::= "#" rest-of-line 052 * indent ::= "| "* ("+" | "\\") "- " 053 * reference ::= "^" id 054 * node ::= coords (range)? space (scope("<" premanagedScope)?)? space "optional"? space 055 * ("relocations=" coords ("," coords)*)? ("(" id ")")? 056 * coords ::= groupId ":" artifactId (":" extension (":" classifier)?)? ":" version 057 * </pre> 058 * 059 * The special token {@code (null)} may be used to indicate an "empty" root node with no dependency. 060 * <p> 061 * If {@code indent} is empty, the line defines the root node. Only one root node may be defined. The level is 062 * calculated by the distance from the beginning of the line. One level is three characters of indentation. 063 * <p> 064 * The {@code ^id} syntax allows to reuse a previously built node to share common sub graphs among different parent 065 * nodes. 066 * <h2>Example</h2> 067 * 068 * <pre> 069 * gid:aid:ver 070 * +- gid:aid2:ver scope 071 * | \- gid:aid3:ver (id1) # assign id for reference below 072 * +- gid:aid4:ext:ver scope 073 * \- ^id1 # reuse previous node 074 * </pre> 075 * 076 * <h2>Multiple definitions in one resource</h2> 077 * <p> 078 * By using {@link #parseMultiResource(String)}, definitions divided by a line beginning with "---" can be read from the 079 * same resource. The rest of the line is ignored. 080 * <h2>Substitutions</h2> 081 * <p> 082 * You may define substitutions (see {@link #setSubstitutions(String...)}, 083 * {@link #DependencyGraphParser(String, Collection)}). Every '%s' in the definition will be substituted by the next 084 * String in the defined substitutions. 085 * <h3>Example</h3> 086 * 087 * <pre> 088 * parser.setSubstitutions( "foo", "bar" ); 089 * String def = "gid:%s:ext:ver\n" + "+- gid:%s:ext:ver"; 090 * </pre> 091 * 092 * The first node will have "foo" as its artifact id, the second node (child to the first) will have "bar" as its 093 * artifact id. 094 */ 095public class DependencyGraphParser { 096 097 private final VersionScheme versionScheme; 098 099 private final String prefix; 100 101 private Collection<String> substitutions; 102 103 /** 104 * Create a parser with the given prefix and the given substitution strings. 105 * 106 * @see DependencyGraphParser#parseResource(String) 107 */ 108 public DependencyGraphParser(String prefix, Collection<String> substitutions) { 109 this.prefix = prefix; 110 this.substitutions = substitutions; 111 versionScheme = new TestVersionScheme(); 112 } 113 114 /** 115 * Create a parser with the given prefix. 116 * 117 * @see DependencyGraphParser#parseResource(String) 118 */ 119 public DependencyGraphParser(String prefix) { 120 this(prefix, Collections.emptyList()); 121 } 122 123 /** 124 * Create a parser with an empty prefix. 125 */ 126 public DependencyGraphParser() { 127 this(""); 128 } 129 130 /** 131 * Parse the given graph definition. 132 */ 133 public DependencyNode parseLiteral(String dependencyGraph) throws IOException { 134 try (BufferedReader reader = new BufferedReader(new StringReader(dependencyGraph))) { 135 return parse(reader); 136 } 137 } 138 139 /** 140 * Parse the graph definition read from the given classpath resource. If a prefix is set, this method will load the 141 * resource from 'prefix + resource'. 142 */ 143 public DependencyNode parseResource(String resource) throws IOException { 144 URL res = this.getClass().getClassLoader().getResource(prefix + resource); 145 if (res == null) { 146 throw new IOException("Could not find classpath resource " + prefix + resource); 147 } 148 return parse(res); 149 } 150 151 /** 152 * Parse multiple graphs in one resource, divided by "---". 153 */ 154 public List<DependencyNode> parseMultiResource(String resource) throws IOException { 155 URL res = this.getClass().getClassLoader().getResource(prefix + resource); 156 if (res == null) { 157 throw new IOException("Could not find classpath resource " + prefix + resource); 158 } 159 160 try (BufferedReader reader = 161 new BufferedReader(new InputStreamReader(res.openStream(), StandardCharsets.UTF_8))) { 162 List<DependencyNode> ret = new ArrayList<>(); 163 DependencyNode root = null; 164 while ((root = parse(reader)) != null) { 165 ret.add(root); 166 } 167 return ret; 168 } 169 } 170 171 /** 172 * Parse the graph definition read from the given URL. 173 */ 174 public DependencyNode parse(URL resource) throws IOException { 175 try (BufferedReader reader = 176 new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8))) { 177 return parse(reader); 178 } 179 } 180 181 private DependencyNode parse(BufferedReader in) throws IOException { 182 Iterator<String> substitutionIterator = (substitutions != null) ? substitutions.iterator() : null; 183 184 String line = null; 185 186 DependencyNode root = null; 187 DependencyNode node = null; 188 int prevLevel = 0; 189 190 Map<String, DependencyNode> nodes = new HashMap<>(); 191 LinkedList<DependencyNode> stack = new LinkedList<>(); 192 boolean isRootNode = true; 193 194 while ((line = in.readLine()) != null) { 195 line = cutComment(line); 196 197 if (isEmpty(line)) { 198 // skip empty line 199 continue; 200 } 201 202 if (isEOFMarker(line)) { 203 // stop parsing 204 break; 205 } 206 207 while (line.contains("%s")) { 208 if (!substitutionIterator.hasNext()) { 209 throw new IllegalStateException("not enough substitutions to fill placeholders"); 210 } 211 line = line.replaceFirst("%s", substitutionIterator.next()); 212 } 213 214 LineContext ctx = createContext(line); 215 if (prevLevel < ctx.getLevel()) { 216 // previous node is new parent 217 stack.add(node); 218 } 219 220 // get to real parent 221 while (prevLevel > ctx.getLevel()) { 222 stack.removeLast(); 223 prevLevel -= 1; 224 } 225 226 prevLevel = ctx.getLevel(); 227 228 if (ctx.getDefinition() != null && ctx.getDefinition().reference != null) { 229 String reference = ctx.getDefinition().reference; 230 DependencyNode child = nodes.get(reference); 231 if (child == null) { 232 throw new IllegalStateException("undefined reference " + reference); 233 } 234 node.getChildren().add(child); 235 } else { 236 237 node = build(isRootNode ? null : stack.getLast(), ctx, isRootNode); 238 239 if (isRootNode) { 240 root = node; 241 isRootNode = false; 242 } 243 244 if (ctx.getDefinition() != null && ctx.getDefinition().id != null) { 245 nodes.put(ctx.getDefinition().id, node); 246 } 247 } 248 } 249 250 return root; 251 } 252 253 private boolean isEOFMarker(String line) { 254 return line.startsWith("---"); 255 } 256 257 private static boolean isEmpty(String line) { 258 return line == null || line.isEmpty(); 259 } 260 261 private static String cutComment(String line) { 262 int idx = line.indexOf('#'); 263 264 if (idx != -1) { 265 line = line.substring(0, idx); 266 } 267 268 return line; 269 } 270 271 private DependencyNode build(DependencyNode parent, LineContext ctx, boolean isRoot) { 272 NodeDefinition def = ctx.getDefinition(); 273 if (!isRoot && parent == null) { 274 throw new IllegalStateException("dangling node: " + def); 275 } else if (ctx.getLevel() == 0 && parent != null) { 276 throw new IllegalStateException("inconsistent leveling (parent for level 0?): " + def); 277 } 278 279 DefaultDependencyNode node; 280 if (def != null) { 281 DefaultArtifact artifact = new DefaultArtifact(def.coords, def.properties); 282 Dependency dependency = new Dependency(artifact, def.scope, def.optional); 283 node = new DefaultDependencyNode(dependency); 284 int managedBits = 0; 285 if (def.premanagedScope != null) { 286 managedBits |= DependencyNode.MANAGED_SCOPE; 287 node.setData("premanaged.scope", def.premanagedScope); 288 } 289 if (def.premanagedVersion != null) { 290 managedBits |= DependencyNode.MANAGED_VERSION; 291 node.setData("premanaged.version", def.premanagedVersion); 292 } 293 node.setManagedBits(managedBits); 294 if (def.relocations != null) { 295 List<Artifact> relocations = new ArrayList<>(); 296 for (String relocation : def.relocations) { 297 relocations.add(new DefaultArtifact(relocation)); 298 } 299 node.setRelocations(relocations); 300 } 301 try { 302 node.setVersion(versionScheme.parseVersion(artifact.getVersion())); 303 node.setVersionConstraint( 304 versionScheme.parseVersionConstraint(def.range != null ? def.range : artifact.getVersion())); 305 } catch (InvalidVersionSpecificationException e) { 306 throw new IllegalArgumentException("bad version: " + e.getMessage(), e); 307 } 308 } else { 309 node = new DefaultDependencyNode((Dependency) null); 310 } 311 312 if (parent != null) { 313 parent.getChildren().add(node); 314 } 315 316 return node; 317 } 318 319 public String dump(DependencyNode root) { 320 StringBuilder ret = new StringBuilder(); 321 322 List<NodeEntry> entries = new ArrayList<>(); 323 324 addNode(root, 0, entries); 325 326 for (NodeEntry nodeEntry : entries) { 327 char[] level = new char[(nodeEntry.getLevel() * 3)]; 328 Arrays.fill(level, ' '); 329 330 if (level.length != 0) { 331 level[level.length - 3] = '+'; 332 level[level.length - 2] = '-'; 333 } 334 335 String definition = nodeEntry.getDefinition(); 336 337 ret.append(level).append(definition).append("\n"); 338 } 339 340 return ret.toString(); 341 } 342 343 private void addNode(DependencyNode root, int level, List<NodeEntry> entries) { 344 345 NodeEntry entry = new NodeEntry(); 346 Dependency dependency = root.getDependency(); 347 StringBuilder defBuilder = new StringBuilder(); 348 if (dependency == null) { 349 defBuilder.append("(null)"); 350 } else { 351 Artifact artifact = dependency.getArtifact(); 352 353 defBuilder 354 .append(artifact.getGroupId()) 355 .append(":") 356 .append(artifact.getArtifactId()) 357 .append(":") 358 .append(artifact.getExtension()) 359 .append(":") 360 .append(artifact.getVersion()); 361 if (dependency.getScope() != null && (!"".equals(dependency.getScope()))) { 362 defBuilder.append(":").append(dependency.getScope()); 363 } 364 365 Map<String, String> properties = artifact.getProperties(); 366 if (!(properties == null || properties.isEmpty())) { 367 for (Map.Entry<String, String> prop : properties.entrySet()) { 368 defBuilder.append(";").append(prop.getKey()).append("=").append(prop.getValue()); 369 } 370 } 371 } 372 373 entry.setDefinition(defBuilder.toString()); 374 entry.setLevel(level++); 375 376 entries.add(entry); 377 378 for (DependencyNode node : root.getChildren()) { 379 addNode(node, level, entries); 380 } 381 } 382 383 private static class NodeEntry { 384 int level; 385 386 String definition; 387 388 Map<String, String> properties; 389 390 public int getLevel() { 391 return level; 392 } 393 394 public void setLevel(int level) { 395 this.level = level; 396 } 397 398 public String getDefinition() { 399 return definition; 400 } 401 402 public void setDefinition(String definition) { 403 this.definition = definition; 404 } 405 406 public Map<String, String> getProperties() { 407 return properties; 408 } 409 410 public void setProperties(Map<String, String> properties) { 411 this.properties = properties; 412 } 413 } 414 415 private static LineContext createContext(String line) { 416 LineContext ctx = new LineContext(); 417 String definition; 418 419 String[] split = line.split("- "); 420 if (split.length == 1) // root 421 { 422 ctx.setLevel(0); 423 definition = split[0]; 424 } else { 425 ctx.setLevel((int) Math.ceil((double) split[0].length() / (double) 3)); 426 definition = split[1]; 427 } 428 429 if ("(null)".equalsIgnoreCase(definition)) { 430 return ctx; 431 } 432 433 ctx.setDefinition(new NodeDefinition(definition)); 434 435 return ctx; 436 } 437 438 static class LineContext { 439 NodeDefinition definition; 440 441 int level; 442 443 public NodeDefinition getDefinition() { 444 return definition; 445 } 446 447 public void setDefinition(NodeDefinition definition) { 448 this.definition = definition; 449 } 450 451 public int getLevel() { 452 return level; 453 } 454 455 public void setLevel(int level) { 456 this.level = level; 457 } 458 } 459 460 public Collection<String> getSubstitutions() { 461 return substitutions; 462 } 463 464 public void setSubstitutions(Collection<String> substitutions) { 465 this.substitutions = substitutions; 466 } 467 468 public void setSubstitutions(String... substitutions) { 469 setSubstitutions(Arrays.asList(substitutions)); 470 } 471}