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