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