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 ("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 098 private final VersionScheme versionScheme; 099 100 private final String prefix; 101 102 private Collection<String> substitutions; 103 104 /** 105 * Create a parser with the given prefix and the given substitution strings. 106 * 107 * @see DependencyGraphParser#parseResource(String) 108 */ 109 public DependencyGraphParser( String prefix, Collection<String> substitutions ) 110 { 111 this.prefix = prefix; 112 this.substitutions = substitutions; 113 versionScheme = new TestVersionScheme(); 114 } 115 116 /** 117 * Create a parser with the given prefix. 118 * 119 * @see DependencyGraphParser#parseResource(String) 120 */ 121 public DependencyGraphParser( String prefix ) 122 { 123 this( prefix, Collections.<String>emptyList() ); 124 } 125 126 /** 127 * Create a parser with an empty prefix. 128 */ 129 public DependencyGraphParser() 130 { 131 this( "" ); 132 } 133 134 /** 135 * Parse the given graph definition. 136 */ 137 public DependencyNode parseLiteral( String dependencyGraph ) 138 throws IOException 139 { 140 BufferedReader reader = new BufferedReader( new StringReader( dependencyGraph ) ); 141 DependencyNode node = parse( reader ); 142 reader.close(); 143 return node; 144 } 145 146 /** 147 * Parse the graph definition read from the given classpath resource. If a prefix is set, this method will load the 148 * resource from 'prefix + resource'. 149 */ 150 public DependencyNode parseResource( String resource ) 151 throws IOException 152 { 153 URL res = this.getClass().getClassLoader().getResource( prefix + resource ); 154 if ( res == null ) 155 { 156 throw new IOException( "Could not find classpath resource " + prefix + resource ); 157 } 158 return parse( res ); 159 } 160 161 /** 162 * Parse multiple graphs in one resource, divided by "---". 163 */ 164 public List<DependencyNode> parseMultiResource( String resource ) 165 throws IOException 166 { 167 URL res = this.getClass().getClassLoader().getResource( prefix + resource ); 168 if ( res == null ) 169 { 170 throw new IOException( "Could not find classpath resource " + prefix + resource ); 171 } 172 173 BufferedReader reader = new BufferedReader( new InputStreamReader( res.openStream(), StandardCharsets.UTF_8 ) ); 174 175 List<DependencyNode> ret = new ArrayList<DependencyNode>(); 176 DependencyNode root = null; 177 while ( ( root = parse( reader ) ) != null ) 178 { 179 ret.add( root ); 180 } 181 return ret; 182 } 183 184 /** 185 * Parse the graph definition read from the given URL. 186 */ 187 public DependencyNode parse( URL resource ) 188 throws IOException 189 { 190 BufferedReader reader = null; 191 try 192 { 193 reader = new BufferedReader( new InputStreamReader( resource.openStream(), StandardCharsets.UTF_8 ) ); 194 final DependencyNode node = parse( reader ); 195 return node; 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<String, DependencyNode>(); 226 LinkedList<DependencyNode> stack = new LinkedList<DependencyNode>(); 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<Artifact>(); 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<NodeEntry>(); 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( ":" ).append( artifact.getExtension() ).append( ":" ).append( artifact.getVersion() ); 429 if ( dependency.getScope() != null && ( !"".equals( dependency.getScope() ) ) ) 430 { 431 defBuilder.append( ":" ).append( dependency.getScope() ); 432 } 433 434 Map<String, String> properties = artifact.getProperties(); 435 if ( !( properties == null || properties.isEmpty() ) ) 436 { 437 for ( Map.Entry<String, String> prop : properties.entrySet() ) 438 { 439 defBuilder.append( ";" ).append( prop.getKey() ).append( "=" ).append( prop.getValue() ); 440 } 441 } 442 } 443 444 entry.setDefinition( defBuilder.toString() ); 445 entry.setLevel( level++ ); 446 447 entries.add( entry ); 448 449 for ( DependencyNode node : root.getChildren() ) 450 { 451 addNode( node, level, entries ); 452 } 453 454 } 455 456 class NodeEntry 457 { 458 int level; 459 460 String definition; 461 462 Map<String, String> properties; 463 464 public int getLevel() 465 { 466 return level; 467 } 468 469 public void setLevel( int level ) 470 { 471 this.level = level; 472 } 473 474 public String getDefinition() 475 { 476 return definition; 477 } 478 479 public void setDefinition( String definition ) 480 { 481 this.definition = definition; 482 } 483 484 public Map<String, String> getProperties() 485 { 486 return properties; 487 } 488 489 public void setProperties( Map<String, String> properties ) 490 { 491 this.properties = properties; 492 } 493 } 494 495 private static LineContext createContext( String line ) 496 { 497 LineContext ctx = new LineContext(); 498 String definition; 499 500 String[] split = line.split( "- " ); 501 if ( split.length == 1 ) // root 502 { 503 ctx.setLevel( 0 ); 504 definition = split[0]; 505 } 506 else 507 { 508 ctx.setLevel( (int) Math.ceil( (double) split[0].length() / (double) 3 ) ); 509 definition = split[1]; 510 } 511 512 if ( "(null)".equalsIgnoreCase( definition ) ) 513 { 514 return ctx; 515 } 516 517 ctx.setDefinition( new NodeDefinition( definition ) ); 518 519 return ctx; 520 } 521 522 static class LineContext 523 { 524 NodeDefinition definition; 525 526 int level; 527 528 public NodeDefinition getDefinition() 529 { 530 return definition; 531 } 532 533 public void setDefinition( NodeDefinition definition ) 534 { 535 this.definition = definition; 536 } 537 538 public int getLevel() 539 { 540 return level; 541 } 542 543 public void setLevel( int level ) 544 { 545 this.level = level; 546 } 547 } 548 549 public Collection<String> getSubstitutions() 550 { 551 return substitutions; 552 } 553 554 public void setSubstitutions( Collection<String> substitutions ) 555 { 556 this.substitutions = substitutions; 557 } 558 559 public void setSubstitutions( String... substitutions ) 560 { 561 setSubstitutions( Arrays.asList( substitutions ) ); 562 } 563 564}