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("&lt;" 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( &quot;foo&quot;, &quot;bar&quot; );
089 * String def = &quot;gid:%s:ext:ver\n&quot; + &quot;+- gid:%s:ext:ver&quot;;
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}