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.InputStream;
25  import java.io.InputStreamReader;
26  import java.io.StringReader;
27  import java.net.URL;
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(), "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 }