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