Project: aether-core
/*******************************************************************************
 * Copyright (c) 2010, 2011 Sonatype, Inc. 
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Eclipse Public License v1.0 
 * which accompanies this distribution, and is available at 
 * http://www.eclipse.org/legal/epl-v10.html 
 * 
 * Contributors: 
 *    Sonatype, Inc. - initial API and implementation 
 *******************************************************************************/
package org.eclipse.aether.internal.test.util; 
 
import java.io.BufferedReader; 
import java.io.IOException; 
import java.io.InputStream; 
import java.io.InputStreamReader; 
import java.io.StringReader; 
import java.net.URL; 
import java.util.ArrayList; 
import java.util.Arrays; 
import java.util.Collection; 
import java.util.HashMap; 
import java.util.Iterator; 
import java.util.LinkedList; 
import java.util.List; 
import java.util.Map; 
 
import org.eclipse.aether.artifact.Artifact; 
import org.eclipse.aether.graph.Dependency; 
import org.eclipse.aether.graph.DependencyNode; 
 
/**
 * Creates a dependency tree from a text description. <h2>Definition</h2> The description format is based on 'mvn 
 * dependency:tree'. Line format: 
 *  
 * <pre> 
 * [level]dependencyDefinition[;key=value;key=value;...] 
 * </pre> 
 *  
 * A <code>dependencyDefinition</code> is of the form: 
 *  
 * <pre> 
 * (id|[(id)]gid:aid:ext:ver[:scope]) 
 * </pre> 
 *  
 * It may also be <code>(null)</code> to indicate an "empty" node with no dependency. 
 * <p> 
 * <h2>Levels</h2> 
 * <p> 
 * If <code>level</code> is empty, the line defines the root node. Only one root node may be defined. The level is 
 * calculated by the distance from the beginning of the line. One level is three characters. A level definition has to 
 * follow this format: 
 *  
 * <pre> 
 * '[| ]*[+\\]- ' 
 * </pre> 
 *  
 * <h2>ID</h2> An ID may be used to reference a previously built node. An ID is of the form: 
 *  
 * <pre> 
 * '[0-9a-zA-Z]+' 
 * </pre> 
 *  
 * To define a node with an ID, prefix the definition with an id in parens: 
 *  
 * <pre> 
 * (id)gid:aid:ext:ver 
 * </pre> 
 *  
 * To insert a previously defined node into the graph, use a caret followed by the ID: 
 *  
 * <pre> 
 * ^id 
 * </pre> 
 *  
 * <h2>Comments</h2> 
 * <p> 
 * A hash starts a comment. A comment ends with the end of the line. Empty lines are ignored. 
 * <h2>Example</h2> 
 *  
 * <pre> 
 * gid:aid:ext:ver 
 * +- gid:aid2:ext:ver:scope 
 * |  \- (id1)gid:aid3:ext:ver 
 * +- gid:aid4:ext:ver:scope 
 * \- ^id1 
 * </pre> 
 *  
 * <h2>Multiple definitions in one resource</h2> 
 * <p> 
 * By using {@link #parseMultiple(String)}, definitions divided by a line beginning with "---" can be read from the same 
 * resource. The rest of the line is ignored. 
 * <h2>Substitutions</h2> 
 * <p> 
 * You may define substitutions (see {@link #setSubstitutions(String...)}, 
 * {@link #DependencyGraphParser(String, Collection)}). Every '%s' in the definition will be substituted by the next 
 * String in the defined substitutions. 
 * <h3>Example</h3> 
 *  
 * <pre> 
 * parser.setSubstitutions( "foo", "bar" ); 
 * String def = "gid:%s:ext:ver\n" + "+- gid:%s:ext:ver"; 
 * </pre> 
 *  
 * The first node will have "foo" as its artifact id, the second node (child to the first) will have "bar" as its 
 * artifact id. 
 */
 
public class DependencyGraphParser 
    private Map<String, DependencyNode> nodes = new HashMap<String, DependencyNode>(); 
 
    private String prefix = ""
 
    private Collection<String> substitutions; 
 
    private Iterator<String> substitutionIterator; 
 
    /**
     * Parse the given graph definition. 
     */
 
    public DependencyNode parseLiteral( String dependencyGraph ) 
        throws IOException 
    { 
        BufferedReader reader = new BufferedReader( new StringReader( dependencyGraph ) ); 
        DependencyNode node = parse( reader ); 
        reader.close(); 
        return node; 
    } 
 
    /**
     * Create a parser with the given prefix and the given substitution strings. 
     *  
     * @see DependencyGraphParser#parse(String) 
     */
 
    public DependencyGraphParser( String prefix, Collection<String> substitutions ) 
    { 
        this.prefix = prefix; 
        this.substitutions = substitutions; 
    } 
 
    /**
     * Create a parser with the given prefix. 
     *  
     * @see DependencyGraphParser#parse(String) 
     */
 
    public DependencyGraphParser( String prefix ) 
    { 
        this( prefix, null ); 
    } 
 
    /**
     * Create a parser with an empty prefix. 
     */
 
    public DependencyGraphParser() 
    { 
        this"" ); 
    } 
 
    /**
     * Parse the graph definition read from the given resource. If a prefix is set, this method will load the resource 
     * from 'prefix + resource'. 
     */
 
    public DependencyNode parse( String resource ) 
        throws IOException 
    { 
        URL res = this.getClass().getClassLoader().getResource( prefix + resource ); 
        if ( res == null ) 
        { 
            throw new IOException( "Could not find classpath resource " + prefix + resource ); 
        } 
        return parse( res ); 
    } 
 
    /**
     * Parse multiple graphs in one resource, divided by "---". 
     */
 
    public List<DependencyNode> parseMultiple( String resource ) 
        throws IOException 
    { 
        URL res = this.getClass().getClassLoader().getResource( prefix + resource ); 
        if ( res == null ) 
        { 
            throw new IOException( "Could not find classpath resource " + prefix + resource ); 
        } 
 
        BufferedReader reader = new BufferedReader( new InputStreamReader( res.openStream(), "UTF-8" ) ); 
 
        List<DependencyNode> ret = new ArrayList<DependencyNode>(); 
        DependencyNode root = null
        while ( ( root = parse( reader ) ) != null ) 
        { 
            ret.add( root ); 
        } 
        return ret; 
    } 
 
    /**
     * Parse the graph definition read from the given URL. 
     */
 
    public DependencyNode parse( URL resource ) 
        throws IOException 
    { 
        InputStream stream = null
        try 
        { 
            stream = resource.openStream(); 
            return parse( new BufferedReader( new InputStreamReader( stream, "UTF-8" ) ) ); 
        } 
        finally 
        { 
            if ( stream != null ) 
            { 
                stream.close(); 
            } 
        } 
    } 
 
    private DependencyNode parse( BufferedReader in ) 
        throws IOException 
    { 
 
        if ( substitutions != null ) 
        { 
            substitutionIterator = substitutions.iterator(); 
        } 
 
        String line = null
 
        DependencyNode root = null
        DependencyNode node = null
        int prevLevel = 0
 
        LinkedList<DependencyNode> stack = new LinkedList<DependencyNode>(); 
        boolean isRootNode = true
 
        while ( ( line = in.readLine() ) != null ) 
        { 
            line = cutComment( line ); 
 
            if ( isEmpty( line ) ) 
            { 
                // skip empty line 
                continue
            } 
 
            if ( isEOFMarker( line ) ) 
            { 
                // stop parsing 
                break
            } 
 
            while ( line.contains( "%s" ) ) 
            { 
                if ( !substitutionIterator.hasNext() ) 
                { 
                    throw new IllegalArgumentException( "not enough substitutions to fill placeholders" ); 
                } 
                line = line.replaceFirst( "%s", substitutionIterator.next() ); 
            } 
 
            LineContext ctx = createContext( line ); 
            if ( prevLevel < ctx.getLevel() ) 
            { 
                // previous node is new parent 
                stack.add( node ); 
            } 
 
            // get to real parent 
            while ( prevLevel > ctx.getLevel() ) 
            { 
                stack.removeLast(); 
                prevLevel -= 1
            } 
 
            prevLevel = ctx.getLevel(); 
 
            if ( ctx.getDefinition() != null && ctx.getDefinition().isReference() ) 
            { 
                DependencyNode child = reference( ctx.getDefinition().getReference() ); 
                node.getChildren().add( child ); 
                node = child; 
            } 
            else 
            { 
 
                node = build( isRootNode ? null : stack.getLast(), ctx, isRootNode ); 
 
                if ( isRootNode ) 
                { 
                    root = node; 
                    isRootNode = false
                } 
 
                if ( ctx.getDefinition() != null && ctx.getDefinition().hasId() ) 
                { 
                    this.nodes.put( ctx.getDefinition().getId(), node ); 
                } 
            } 
        } 
 
        this.nodes.clear(); 
 
        return root; 
 
    } 
 
    private boolean isEOFMarker( String line ) 
    { 
        return line.startsWith( "---" ); 
    } 
 
    private DependencyNode reference( String reference ) 
    { 
        if ( !nodes.containsKey( reference ) ) 
        { 
            throw new IllegalArgumentException( "undefined reference " + reference ); 
        } 
 
        return this.nodes.get( reference ); 
    } 
 
    private static boolean isEmpty( String line ) 
    { 
        return line == null || line.length() == 0
    } 
 
    private static String cutComment( String line ) 
    { 
        int idx = line.indexOf( '#' ); 
 
        if ( idx != -1 ) 
        { 
            line = line.substring( 0, idx ); 
        } 
 
        return line; 
    } 
 
    private DependencyNode build( DependencyNode parent, LineContext ctx, boolean isRoot ) 
    { 
        ArtifactDefinition def = ctx.getDefinition(); 
        if ( !isRoot && parent == null ) 
        { 
            throw new IllegalArgumentException( "dangling node: " + def ); 
        } 
        else if ( ctx.getLevel() == 0 && parent != null ) 
        { 
            throw new IllegalArgumentException( "inconsistent leveling (parent for level 0?): " + def ); 
        } 
 
        NodeBuilder builder = new NodeBuilder(); 
 
        if ( def != null ) 
        { 
            builder.artifactId( def.getArtifactId() ).groupId( def.getGroupId() ); 
            builder.ext( def.getExtension() ).version( def.getVersion() ).scope( def.getScope() ); 
            builder.properties( ctx.getProperties() ); 
        } 
        DependencyNode node = builder.build(); 
 
        if ( parent != null ) 
        { 
            parent.getChildren().add( node ); 
        } 
 
        return node; 
    } 
 
    public String dump( DependencyNode root ) 
    { 
        StringBuilder ret = new StringBuilder(); 
 
        List<NodeEntry> entries = new ArrayList<NodeEntry>(); 
 
        addNode( root, 0, entries ); 
 
        for ( NodeEntry nodeEntry : entries ) 
        { 
            char[] level = new char[( nodeEntry.getLevel() * 3 )]; 
            Arrays.fill( level, ' ' ); 
 
            if ( level.length != 0 ) 
            { 
                level[level.length - 3] = '+'; 
                level[level.length - 2] = '-'; 
            } 
 
            String definition = nodeEntry.getDefinition(); 
 
            ret.append( level ).append( definition ).append( "\n" ); 
        } 
 
        return ret.toString(); 
 
    } 
 
    private void addNode( DependencyNode root, int level, List<NodeEntry> entries ) 
    { 
 
        NodeEntry entry = new NodeEntry(); 
        Dependency dependency = root.getDependency(); 
        StringBuilder defBuilder = new StringBuilder(); 
        if ( dependency == null ) 
        { 
            defBuilder.append( "(null)" ); 
        } 
        else 
        { 
            Artifact artifact = dependency.getArtifact(); 
 
            defBuilder.append( artifact.getGroupId() ).append( ":" ).append( artifact.getArtifactId() ).append( ":" ).append( artifact.getExtension() ).append( ":" ).append( artifact.getVersion() ); 
            if ( dependency.getScope() != null && ( !"".equals( dependency.getScope() ) ) ) 
            { 
                defBuilder.append( ":" ).append( dependency.getScope() ); 
            } 
 
            Map<String, String> properties = artifact.getProperties(); 
            if ( !( properties == null || properties.isEmpty() ) ) 
            { 
                for ( Map.Entry<String, String> prop : properties.entrySet() ) 
                { 
                    defBuilder.append( ";" ).append( prop.getKey() ).append( "=" ).append( prop.getValue() ); 
                } 
            } 
        } 
 
        entry.setDefinition( defBuilder.toString() ); 
        entry.setLevel( level++ ); 
 
        entries.add( entry ); 
 
        for ( DependencyNode node : root.getChildren() ) 
        { 
            addNode( node, level, entries ); 
        } 
 
    } 
 
    class NodeEntry 
    { 
        int level; 
 
        String definition; 
 
        Map<String, String> properties; 
 
        public int getLevel() 
        { 
            return level; 
        } 
 
        public void setLevelint level ) 
        { 
            this.level = level; 
        } 
 
        public String getDefinition() 
        { 
            return definition; 
        } 
 
        public void setDefinition( String definition ) 
        { 
            this.definition = definition; 
        } 
 
        public Map<String, String> getProperties() 
        { 
            return properties; 
        } 
 
        public void setProperties( Map<String, String> properties ) 
        { 
            this.properties = properties; 
        } 
    } 
 
    private static LineContext createContext( String line ) 
    { 
        LineContext ctx = new LineContext(); 
        String definition; 
 
        String[] split = line.split( "- " ); 
        if ( split.length == 1 ) // root 
        { 
            ctx.setLevel( 0 ); 
            definition = split[0]; 
        } 
        else 
        { 
            ctx.setLevel( (int) Math.ceil( (double) split[0].length() / (double3 ) ); 
            definition = split[1]; 
        } 
 
        if ( "(null)".equalsIgnoreCase( definition ) ) 
        { 
            return ctx; 
        } 
 
        split = definition.split( ";" ); 
        ctx.setDefinition( new ArtifactDefinition( split[0] ) ); 
 
        if ( split.length > 1 ) // properties 
        { 
            Map<String, String> props = new HashMap<String, String>(); 
            for ( int i = 1; i < split.length; i++ ) 
            { 
                String[] keyValue = split[i].split( "=" ); 
                String key = keyValue[0]; 
                String value = keyValue[1]; 
                props.put( key, value ); 
            } 
            ctx.setProperties( props ); 
        } 
 
        return ctx; 
    } 
 
    static class LineContext 
    { 
        ArtifactDefinition definition; 
 
        private Map<String, String> properties; 
 
        int level; 
 
        public ArtifactDefinition getDefinition() 
        { 
            return definition; 
        } 
 
        public void setDefinition( ArtifactDefinition definition ) 
        { 
            this.definition = definition; 
        } 
 
        public Map<String, String> getProperties() 
        { 
            return properties; 
        } 
 
        public void setProperties( Map<String, String> properties ) 
        { 
            this.properties = properties; 
        } 
 
        public int getLevel() 
        { 
            return level; 
        } 
 
        public void setLevelint level ) 
        { 
            this.level = level; 
        } 
    } 
 
    public Collection<String> getSubstitutions() 
    { 
        return substitutions; 
    } 
 
    public void setSubstitutions( Collection<String> substitutions ) 
    { 
        this.substitutions = substitutions; 
    } 
 
    public void setSubstitutions( String... substitutions ) 
    { 
        this.setSubstitutions( Arrays.asList( substitutions ) ); 
 
    } 
 
}