View Javadoc
1   package org.eclipse.aether.internal.test.util;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   * 
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   * 
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.BufferedReader;
23  import java.io.IOException;
24  import java.io.InputStreamReader;
25  import java.io.StringReader;
26  import java.net.URL;
27  import java.nio.charset.StandardCharsets;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.Collection;
31  import java.util.Collections;
32  import java.util.HashMap;
33  import java.util.Iterator;
34  import java.util.LinkedList;
35  import java.util.List;
36  import java.util.Map;
37  
38  import org.eclipse.aether.artifact.Artifact;
39  import org.eclipse.aether.artifact.DefaultArtifact;
40  import org.eclipse.aether.graph.DefaultDependencyNode;
41  import org.eclipse.aether.graph.Dependency;
42  import org.eclipse.aether.graph.DependencyNode;
43  import org.eclipse.aether.version.InvalidVersionSpecificationException;
44  import org.eclipse.aether.version.VersionScheme;
45  
46  /**
47   * Creates a dependency graph from a text description. <h2>Definition</h2> Each (non-empty) line in the input defines
48   * one node of the resulting graph:
49   * 
50   * <pre>
51   * line      ::= (indent? ("(null)" | node | reference))? comment?
52   * comment   ::= "#" rest-of-line
53   * indent    ::= "|  "*  ("+" | "\\") "- "
54   * reference ::= "^" id
55   * node      ::= coords (range)? space (scope("&lt;" premanagedScope)?)? space "optional"? space ("relocations=" coords ("," coords)*)? ("(" id ")")?
56   * coords    ::= groupId ":" artifactId (":" extension (":" classifier)?)? ":" version
57   * </pre>
58   * 
59   * The special token {@code (null)} may be used to indicate an "empty" root node with no dependency.
60   * <p>
61   * If {@code indent} is empty, the line defines the root node. Only one root node may be defined. The level is
62   * calculated by the distance from the beginning of the line. One level is three characters of indentation.
63   * <p>
64   * The {@code ^id} syntax allows to reuse a previously built node to share common sub graphs among different parent
65   * nodes.
66   * <h2>Example</h2>
67   * 
68   * <pre>
69   * gid:aid:ver
70   * +- gid:aid2:ver scope
71   * |  \- gid:aid3:ver        (id1)    # assign id for reference below
72   * +- gid:aid4:ext:ver scope
73   * \- ^id1                            # reuse previous node
74   * </pre>
75   * 
76   * <h2>Multiple definitions in one resource</h2>
77   * <p>
78   * By using {@link #parseMultiResource(String)}, definitions divided by a line beginning with "---" can be read from the
79   * same resource. The rest of the line is ignored.
80   * <h2>Substitutions</h2>
81   * <p>
82   * You may define substitutions (see {@link #setSubstitutions(String...)},
83   * {@link #DependencyGraphParser(String, Collection)}). Every '%s' in the definition will be substituted by the next
84   * String in the defined substitutions.
85   * <h3>Example</h3>
86   * 
87   * <pre>
88   * parser.setSubstitutions( &quot;foo&quot;, &quot;bar&quot; );
89   * String def = &quot;gid:%s:ext:ver\n&quot; + &quot;+- gid:%s:ext:ver&quot;;
90   * </pre>
91   * 
92   * The first node will have "foo" as its artifact id, the second node (child to the first) will have "bar" as its
93   * artifact id.
94   */
95  public class DependencyGraphParser
96  {
97  
98      private final VersionScheme versionScheme;
99  
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 }