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 BufferedReader reader = new BufferedReader(new StringReader(dependencyGraph)); 135 DependencyNode node = parse(reader); 136 reader.close(); 137 return node; 138 } 139 140 /** 141 * Parse the graph definition read from the given classpath resource. If a prefix is set, this method will load the 142 * resource from 'prefix + resource'. 143 */ 144 public DependencyNode parseResource(String resource) throws IOException { 145 URL res = this.getClass().getClassLoader().getResource(prefix + resource); 146 if (res == null) { 147 throw new IOException("Could not find classpath resource " + prefix + resource); 148 } 149 return parse(res); 150 } 151 152 /** 153 * Parse multiple graphs in one resource, divided by "---". 154 */ 155 public List<DependencyNode> parseMultiResource(String resource) throws IOException { 156 URL res = this.getClass().getClassLoader().getResource(prefix + resource); 157 if (res == null) { 158 throw new IOException("Could not find classpath resource " + prefix + resource); 159 } 160 161 try (BufferedReader reader = 162 new BufferedReader(new InputStreamReader(res.openStream(), StandardCharsets.UTF_8))) { 163 List<DependencyNode> ret = new ArrayList<>(); 164 DependencyNode root = null; 165 while ((root = parse(reader)) != null) { 166 ret.add(root); 167 } 168 return ret; 169 } 170 } 171 172 /** 173 * Parse the graph definition read from the given URL. 174 */ 175 public DependencyNode parse(URL resource) throws IOException { 176 BufferedReader reader = null; 177 try { 178 reader = new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8)); 179 return parse(reader); 180 } finally { 181 try { 182 if (reader != null) { 183 reader.close(); 184 reader = null; 185 } 186 } catch (final IOException e) { 187 // Suppressed due to an exception already thrown in the try block. 188 } 189 } 190 } 191 192 private DependencyNode parse(BufferedReader in) throws IOException { 193 Iterator<String> substitutionIterator = (substitutions != null) ? substitutions.iterator() : null; 194 195 String line = null; 196 197 DependencyNode root = null; 198 DependencyNode node = null; 199 int prevLevel = 0; 200 201 Map<String, DependencyNode> nodes = new HashMap<>(); 202 LinkedList<DependencyNode> stack = new LinkedList<>(); 203 boolean isRootNode = true; 204 205 while ((line = in.readLine()) != null) { 206 line = cutComment(line); 207 208 if (isEmpty(line)) { 209 // skip empty line 210 continue; 211 } 212 213 if (isEOFMarker(line)) { 214 // stop parsing 215 break; 216 } 217 218 while (line.contains("%s")) { 219 if (!substitutionIterator.hasNext()) { 220 throw new IllegalStateException("not enough substitutions to fill placeholders"); 221 } 222 line = line.replaceFirst("%s", substitutionIterator.next()); 223 } 224 225 LineContext ctx = createContext(line); 226 if (prevLevel < ctx.getLevel()) { 227 // previous node is new parent 228 stack.add(node); 229 } 230 231 // get to real parent 232 while (prevLevel > ctx.getLevel()) { 233 stack.removeLast(); 234 prevLevel -= 1; 235 } 236 237 prevLevel = ctx.getLevel(); 238 239 if (ctx.getDefinition() != null && ctx.getDefinition().reference != null) { 240 String reference = ctx.getDefinition().reference; 241 DependencyNode child = nodes.get(reference); 242 if (child == null) { 243 throw new IllegalStateException("undefined reference " + reference); 244 } 245 node.getChildren().add(child); 246 } else { 247 248 node = build(isRootNode ? null : stack.getLast(), ctx, isRootNode); 249 250 if (isRootNode) { 251 root = node; 252 isRootNode = false; 253 } 254 255 if (ctx.getDefinition() != null && ctx.getDefinition().id != null) { 256 nodes.put(ctx.getDefinition().id, node); 257 } 258 } 259 } 260 261 return root; 262 } 263 264 private boolean isEOFMarker(String line) { 265 return line.startsWith("---"); 266 } 267 268 private static boolean isEmpty(String line) { 269 return line == null || line.isEmpty(); 270 } 271 272 private static String cutComment(String line) { 273 int idx = line.indexOf('#'); 274 275 if (idx != -1) { 276 line = line.substring(0, idx); 277 } 278 279 return line; 280 } 281 282 private DependencyNode build(DependencyNode parent, LineContext ctx, boolean isRoot) { 283 NodeDefinition def = ctx.getDefinition(); 284 if (!isRoot && parent == null) { 285 throw new IllegalStateException("dangling node: " + def); 286 } else if (ctx.getLevel() == 0 && parent != null) { 287 throw new IllegalStateException("inconsistent leveling (parent for level 0?): " + def); 288 } 289 290 DefaultDependencyNode node; 291 if (def != null) { 292 DefaultArtifact artifact = new DefaultArtifact(def.coords, def.properties); 293 Dependency dependency = new Dependency(artifact, def.scope, def.optional); 294 node = new DefaultDependencyNode(dependency); 295 int managedBits = 0; 296 if (def.premanagedScope != null) { 297 managedBits |= DependencyNode.MANAGED_SCOPE; 298 node.setData("premanaged.scope", def.premanagedScope); 299 } 300 if (def.premanagedVersion != null) { 301 managedBits |= DependencyNode.MANAGED_VERSION; 302 node.setData("premanaged.version", def.premanagedVersion); 303 } 304 node.setManagedBits(managedBits); 305 if (def.relocations != null) { 306 List<Artifact> relocations = new ArrayList<>(); 307 for (String relocation : def.relocations) { 308 relocations.add(new DefaultArtifact(relocation)); 309 } 310 node.setRelocations(relocations); 311 } 312 try { 313 node.setVersion(versionScheme.parseVersion(artifact.getVersion())); 314 node.setVersionConstraint( 315 versionScheme.parseVersionConstraint(def.range != null ? def.range : artifact.getVersion())); 316 } catch (InvalidVersionSpecificationException e) { 317 throw new IllegalArgumentException("bad version: " + e.getMessage(), e); 318 } 319 } else { 320 node = new DefaultDependencyNode((Dependency) null); 321 } 322 323 if (parent != null) { 324 parent.getChildren().add(node); 325 } 326 327 return node; 328 } 329 330 public String dump(DependencyNode root) { 331 StringBuilder ret = new StringBuilder(); 332 333 List<NodeEntry> entries = new ArrayList<>(); 334 335 addNode(root, 0, entries); 336 337 for (NodeEntry nodeEntry : entries) { 338 char[] level = new char[(nodeEntry.getLevel() * 3)]; 339 Arrays.fill(level, ' '); 340 341 if (level.length != 0) { 342 level[level.length - 3] = '+'; 343 level[level.length - 2] = '-'; 344 } 345 346 String definition = nodeEntry.getDefinition(); 347 348 ret.append(level).append(definition).append("\n"); 349 } 350 351 return ret.toString(); 352 } 353 354 private void addNode(DependencyNode root, int level, List<NodeEntry> entries) { 355 356 NodeEntry entry = new NodeEntry(); 357 Dependency dependency = root.getDependency(); 358 StringBuilder defBuilder = new StringBuilder(); 359 if (dependency == null) { 360 defBuilder.append("(null)"); 361 } else { 362 Artifact artifact = dependency.getArtifact(); 363 364 defBuilder 365 .append(artifact.getGroupId()) 366 .append(":") 367 .append(artifact.getArtifactId()) 368 .append(":") 369 .append(artifact.getExtension()) 370 .append(":") 371 .append(artifact.getVersion()); 372 if (dependency.getScope() != null && (!"".equals(dependency.getScope()))) { 373 defBuilder.append(":").append(dependency.getScope()); 374 } 375 376 Map<String, String> properties = artifact.getProperties(); 377 if (!(properties == null || properties.isEmpty())) { 378 for (Map.Entry<String, String> prop : properties.entrySet()) { 379 defBuilder.append(";").append(prop.getKey()).append("=").append(prop.getValue()); 380 } 381 } 382 } 383 384 entry.setDefinition(defBuilder.toString()); 385 entry.setLevel(level++); 386 387 entries.add(entry); 388 389 for (DependencyNode node : root.getChildren()) { 390 addNode(node, level, entries); 391 } 392 } 393 394 private static class NodeEntry { 395 int level; 396 397 String definition; 398 399 Map<String, String> properties; 400 401 public int getLevel() { 402 return level; 403 } 404 405 public void setLevel(int level) { 406 this.level = level; 407 } 408 409 public String getDefinition() { 410 return definition; 411 } 412 413 public void setDefinition(String definition) { 414 this.definition = definition; 415 } 416 417 public Map<String, String> getProperties() { 418 return properties; 419 } 420 421 public void setProperties(Map<String, String> properties) { 422 this.properties = properties; 423 } 424 } 425 426 private static LineContext createContext(String line) { 427 LineContext ctx = new LineContext(); 428 String definition; 429 430 String[] split = line.split("- "); 431 if (split.length == 1) // root 432 { 433 ctx.setLevel(0); 434 definition = split[0]; 435 } else { 436 ctx.setLevel((int) Math.ceil((double) split[0].length() / (double) 3)); 437 definition = split[1]; 438 } 439 440 if ("(null)".equalsIgnoreCase(definition)) { 441 return ctx; 442 } 443 444 ctx.setDefinition(new NodeDefinition(definition)); 445 446 return ctx; 447 } 448 449 static class LineContext { 450 NodeDefinition definition; 451 452 int level; 453 454 public NodeDefinition getDefinition() { 455 return definition; 456 } 457 458 public void setDefinition(NodeDefinition definition) { 459 this.definition = definition; 460 } 461 462 public int getLevel() { 463 return level; 464 } 465 466 public void setLevel(int level) { 467 this.level = level; 468 } 469 } 470 471 public Collection<String> getSubstitutions() { 472 return substitutions; 473 } 474 475 public void setSubstitutions(Collection<String> substitutions) { 476 this.substitutions = substitutions; 477 } 478 479 public void setSubstitutions(String... substitutions) { 480 setSubstitutions(Arrays.asList(substitutions)); 481 } 482}