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
056 *                  ("relocations=" coords ("," coords)*)? ("(" id ")")?
057 * coords    ::= groupId ":" artifactId (":" extension (":" classifier)?)? ":" version
058 * </pre>
059 * 
060 * The special token {@code (null)} may be used to indicate an "empty" root node with no dependency.
061 * <p>
062 * If {@code indent} is empty, the line defines the root node. Only one root node may be defined. The level is
063 * calculated by the distance from the beginning of the line. One level is three characters of indentation.
064 * <p>
065 * The {@code ^id} syntax allows to reuse a previously built node to share common sub graphs among different parent
066 * nodes.
067 * <h2>Example</h2>
068 * 
069 * <pre>
070 * gid:aid:ver
071 * +- gid:aid2:ver scope
072 * |  \- gid:aid3:ver        (id1)    # assign id for reference below
073 * +- gid:aid4:ext:ver scope
074 * \- ^id1                            # reuse previous node
075 * </pre>
076 * 
077 * <h2>Multiple definitions in one resource</h2>
078 * <p>
079 * By using {@link #parseMultiResource(String)}, definitions divided by a line beginning with "---" can be read from the
080 * same resource. The rest of the line is ignored.
081 * <h2>Substitutions</h2>
082 * <p>
083 * You may define substitutions (see {@link #setSubstitutions(String...)},
084 * {@link #DependencyGraphParser(String, Collection)}). Every '%s' in the definition will be substituted by the next
085 * String in the defined substitutions.
086 * <h3>Example</h3>
087 * 
088 * <pre>
089 * parser.setSubstitutions( &quot;foo&quot;, &quot;bar&quot; );
090 * String def = &quot;gid:%s:ext:ver\n&quot; + &quot;+- gid:%s:ext:ver&quot;;
091 * </pre>
092 * 
093 * The first node will have "foo" as its artifact id, the second node (child to the first) will have "bar" as its
094 * artifact id.
095 */
096public class DependencyGraphParser
097{
098
099    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}