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
56   *                  ("relocations=" coords ("," coords)*)? ("(" id ")")?
57   * coords    ::= groupId ":" artifactId (":" extension (":" classifier)?)? ":" version
58   * </pre>
59   * 
60   * The special token {@code (null)} may be used to indicate an "empty" root node with no dependency.
61   * <p>
62   * If {@code indent} is empty, the line defines the root node. Only one root node may be defined. The level is
63   * calculated by the distance from the beginning of the line. One level is three characters of indentation.
64   * <p>
65   * The {@code ^id} syntax allows to reuse a previously built node to share common sub graphs among different parent
66   * nodes.
67   * <h2>Example</h2>
68   * 
69   * <pre>
70   * gid:aid:ver
71   * +- gid:aid2:ver scope
72   * |  \- gid:aid3:ver        (id1)    # assign id for reference below
73   * +- gid:aid4:ext:ver scope
74   * \- ^id1                            # reuse previous node
75   * </pre>
76   * 
77   * <h2>Multiple definitions in one resource</h2>
78   * <p>
79   * By using {@link #parseMultiResource(String)}, definitions divided by a line beginning with "---" can be read from the
80   * same resource. The rest of the line is ignored.
81   * <h2>Substitutions</h2>
82   * <p>
83   * You may define substitutions (see {@link #setSubstitutions(String...)},
84   * {@link #DependencyGraphParser(String, Collection)}). Every '%s' in the definition will be substituted by the next
85   * String in the defined substitutions.
86   * <h3>Example</h3>
87   * 
88   * <pre>
89   * parser.setSubstitutions( &quot;foo&quot;, &quot;bar&quot; );
90   * String def = &quot;gid:%s:ext:ver\n&quot; + &quot;+- gid:%s:ext:ver&quot;;
91   * </pre>
92   * 
93   * The first node will have "foo" as its artifact id, the second node (child to the first) will have "bar" as its
94   * artifact id.
95   */
96  public class DependencyGraphParser
97  {
98  
99      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 }