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.InputStream;
025import java.io.InputStreamReader;
026import java.io.StringReader;
027import java.net.URL;
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(), "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}