001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.internal.test.util;
020
021import java.io.BufferedReader;
022import java.io.IOException;
023import java.io.InputStreamReader;
024import java.io.StringReader;
025import java.net.URL;
026import java.nio.charset.StandardCharsets;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.Iterator;
033import java.util.LinkedList;
034import java.util.List;
035import java.util.Map;
036
037import org.eclipse.aether.artifact.Artifact;
038import org.eclipse.aether.artifact.DefaultArtifact;
039import org.eclipse.aether.graph.DefaultDependencyNode;
040import org.eclipse.aether.graph.Dependency;
041import org.eclipse.aether.graph.DependencyNode;
042import org.eclipse.aether.version.InvalidVersionSpecificationException;
043import org.eclipse.aether.version.VersionScheme;
044
045/**
046 * Creates a dependency graph from a text description. <h2>Definition</h2> Each (non-empty) line in the input defines
047 * one node of the resulting graph:
048 *
049 * <pre>
050 * line      ::= (indent? ("(null)" | node | reference))? comment?
051 * comment   ::= "#" rest-of-line
052 * indent    ::= "|  "*  ("+" | "\\") "- "
053 * reference ::= "^" id
054 * node      ::= coords (range)? space (scope("&lt;" premanagedScope)?)? space "optional"? space
055 *                  ("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    private final VersionScheme versionScheme;
098
099    private final String prefix;
100
101    private Collection<String> substitutions;
102
103    /**
104     * Create a parser with the given prefix and the given substitution strings.
105     *
106     * @see DependencyGraphParser#parseResource(String)
107     */
108    public DependencyGraphParser(String prefix, Collection<String> substitutions) {
109        this.prefix = prefix;
110        this.substitutions = substitutions;
111        versionScheme = new TestVersionScheme();
112    }
113
114    /**
115     * Create a parser with the given prefix.
116     *
117     * @see DependencyGraphParser#parseResource(String)
118     */
119    public DependencyGraphParser(String prefix) {
120        this(prefix, Collections.emptyList());
121    }
122
123    /**
124     * Create a parser with an empty prefix.
125     */
126    public DependencyGraphParser() {
127        this("");
128    }
129
130    /**
131     * Parse the given graph definition.
132     */
133    public DependencyNode parseLiteral(String dependencyGraph) throws IOException {
134        BufferedReader reader = new BufferedReader(new StringReader(dependencyGraph));
135        DependencyNode node = parse(reader);
136        reader.close();
137        return node;
138    }
139
140    /**
141     * Parse the graph definition read from the given classpath resource. If a prefix is set, this method will load the
142     * resource from 'prefix + resource'.
143     */
144    public DependencyNode parseResource(String resource) throws IOException {
145        URL res = this.getClass().getClassLoader().getResource(prefix + resource);
146        if (res == null) {
147            throw new IOException("Could not find classpath resource " + prefix + resource);
148        }
149        return parse(res);
150    }
151
152    /**
153     * Parse multiple graphs in one resource, divided by "---".
154     */
155    public List<DependencyNode> parseMultiResource(String resource) throws IOException {
156        URL res = this.getClass().getClassLoader().getResource(prefix + resource);
157        if (res == null) {
158            throw new IOException("Could not find classpath resource " + prefix + resource);
159        }
160
161        try (BufferedReader reader =
162                new BufferedReader(new InputStreamReader(res.openStream(), StandardCharsets.UTF_8))) {
163            List<DependencyNode> ret = new ArrayList<>();
164            DependencyNode root = null;
165            while ((root = parse(reader)) != null) {
166                ret.add(root);
167            }
168            return ret;
169        }
170    }
171
172    /**
173     * Parse the graph definition read from the given URL.
174     */
175    public DependencyNode parse(URL resource) throws IOException {
176        BufferedReader reader = null;
177        try {
178            reader = new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8));
179            return parse(reader);
180        } finally {
181            try {
182                if (reader != null) {
183                    reader.close();
184                    reader = null;
185                }
186            } catch (final IOException e) {
187                // Suppressed due to an exception already thrown in the try block.
188            }
189        }
190    }
191
192    private DependencyNode parse(BufferedReader in) throws IOException {
193        Iterator<String> substitutionIterator = (substitutions != null) ? substitutions.iterator() : null;
194
195        String line = null;
196
197        DependencyNode root = null;
198        DependencyNode node = null;
199        int prevLevel = 0;
200
201        Map<String, DependencyNode> nodes = new HashMap<>();
202        LinkedList<DependencyNode> stack = new LinkedList<>();
203        boolean isRootNode = true;
204
205        while ((line = in.readLine()) != null) {
206            line = cutComment(line);
207
208            if (isEmpty(line)) {
209                // skip empty line
210                continue;
211            }
212
213            if (isEOFMarker(line)) {
214                // stop parsing
215                break;
216            }
217
218            while (line.contains("%s")) {
219                if (!substitutionIterator.hasNext()) {
220                    throw new IllegalStateException("not enough substitutions to fill placeholders");
221                }
222                line = line.replaceFirst("%s", substitutionIterator.next());
223            }
224
225            LineContext ctx = createContext(line);
226            if (prevLevel < ctx.getLevel()) {
227                // previous node is new parent
228                stack.add(node);
229            }
230
231            // get to real parent
232            while (prevLevel > ctx.getLevel()) {
233                stack.removeLast();
234                prevLevel -= 1;
235            }
236
237            prevLevel = ctx.getLevel();
238
239            if (ctx.getDefinition() != null && ctx.getDefinition().reference != null) {
240                String reference = ctx.getDefinition().reference;
241                DependencyNode child = nodes.get(reference);
242                if (child == null) {
243                    throw new IllegalStateException("undefined reference " + reference);
244                }
245                node.getChildren().add(child);
246            } else {
247
248                node = build(isRootNode ? null : stack.getLast(), ctx, isRootNode);
249
250                if (isRootNode) {
251                    root = node;
252                    isRootNode = false;
253                }
254
255                if (ctx.getDefinition() != null && ctx.getDefinition().id != null) {
256                    nodes.put(ctx.getDefinition().id, node);
257                }
258            }
259        }
260
261        return root;
262    }
263
264    private boolean isEOFMarker(String line) {
265        return line.startsWith("---");
266    }
267
268    private static boolean isEmpty(String line) {
269        return line == null || line.isEmpty();
270    }
271
272    private static String cutComment(String line) {
273        int idx = line.indexOf('#');
274
275        if (idx != -1) {
276            line = line.substring(0, idx);
277        }
278
279        return line;
280    }
281
282    private DependencyNode build(DependencyNode parent, LineContext ctx, boolean isRoot) {
283        NodeDefinition def = ctx.getDefinition();
284        if (!isRoot && parent == null) {
285            throw new IllegalStateException("dangling node: " + def);
286        } else if (ctx.getLevel() == 0 && parent != null) {
287            throw new IllegalStateException("inconsistent leveling (parent for level 0?): " + def);
288        }
289
290        DefaultDependencyNode node;
291        if (def != null) {
292            DefaultArtifact artifact = new DefaultArtifact(def.coords, def.properties);
293            Dependency dependency = new Dependency(artifact, def.scope, def.optional);
294            node = new DefaultDependencyNode(dependency);
295            int managedBits = 0;
296            if (def.premanagedScope != null) {
297                managedBits |= DependencyNode.MANAGED_SCOPE;
298                node.setData("premanaged.scope", def.premanagedScope);
299            }
300            if (def.premanagedVersion != null) {
301                managedBits |= DependencyNode.MANAGED_VERSION;
302                node.setData("premanaged.version", def.premanagedVersion);
303            }
304            node.setManagedBits(managedBits);
305            if (def.relocations != null) {
306                List<Artifact> relocations = new ArrayList<>();
307                for (String relocation : def.relocations) {
308                    relocations.add(new DefaultArtifact(relocation));
309                }
310                node.setRelocations(relocations);
311            }
312            try {
313                node.setVersion(versionScheme.parseVersion(artifact.getVersion()));
314                node.setVersionConstraint(
315                        versionScheme.parseVersionConstraint(def.range != null ? def.range : artifact.getVersion()));
316            } catch (InvalidVersionSpecificationException e) {
317                throw new IllegalArgumentException("bad version: " + e.getMessage(), e);
318            }
319        } else {
320            node = new DefaultDependencyNode((Dependency) null);
321        }
322
323        if (parent != null) {
324            parent.getChildren().add(node);
325        }
326
327        return node;
328    }
329
330    public String dump(DependencyNode root) {
331        StringBuilder ret = new StringBuilder();
332
333        List<NodeEntry> entries = new ArrayList<>();
334
335        addNode(root, 0, entries);
336
337        for (NodeEntry nodeEntry : entries) {
338            char[] level = new char[(nodeEntry.getLevel() * 3)];
339            Arrays.fill(level, ' ');
340
341            if (level.length != 0) {
342                level[level.length - 3] = '+';
343                level[level.length - 2] = '-';
344            }
345
346            String definition = nodeEntry.getDefinition();
347
348            ret.append(level).append(definition).append("\n");
349        }
350
351        return ret.toString();
352    }
353
354    private void addNode(DependencyNode root, int level, List<NodeEntry> entries) {
355
356        NodeEntry entry = new NodeEntry();
357        Dependency dependency = root.getDependency();
358        StringBuilder defBuilder = new StringBuilder();
359        if (dependency == null) {
360            defBuilder.append("(null)");
361        } else {
362            Artifact artifact = dependency.getArtifact();
363
364            defBuilder
365                    .append(artifact.getGroupId())
366                    .append(":")
367                    .append(artifact.getArtifactId())
368                    .append(":")
369                    .append(artifact.getExtension())
370                    .append(":")
371                    .append(artifact.getVersion());
372            if (dependency.getScope() != null && (!"".equals(dependency.getScope()))) {
373                defBuilder.append(":").append(dependency.getScope());
374            }
375
376            Map<String, String> properties = artifact.getProperties();
377            if (!(properties == null || properties.isEmpty())) {
378                for (Map.Entry<String, String> prop : properties.entrySet()) {
379                    defBuilder.append(";").append(prop.getKey()).append("=").append(prop.getValue());
380                }
381            }
382        }
383
384        entry.setDefinition(defBuilder.toString());
385        entry.setLevel(level++);
386
387        entries.add(entry);
388
389        for (DependencyNode node : root.getChildren()) {
390            addNode(node, level, entries);
391        }
392    }
393
394    private static class NodeEntry {
395        int level;
396
397        String definition;
398
399        Map<String, String> properties;
400
401        public int getLevel() {
402            return level;
403        }
404
405        public void setLevel(int level) {
406            this.level = level;
407        }
408
409        public String getDefinition() {
410            return definition;
411        }
412
413        public void setDefinition(String definition) {
414            this.definition = definition;
415        }
416
417        public Map<String, String> getProperties() {
418            return properties;
419        }
420
421        public void setProperties(Map<String, String> properties) {
422            this.properties = properties;
423        }
424    }
425
426    private static LineContext createContext(String line) {
427        LineContext ctx = new LineContext();
428        String definition;
429
430        String[] split = line.split("- ");
431        if (split.length == 1) // root
432        {
433            ctx.setLevel(0);
434            definition = split[0];
435        } else {
436            ctx.setLevel((int) Math.ceil((double) split[0].length() / (double) 3));
437            definition = split[1];
438        }
439
440        if ("(null)".equalsIgnoreCase(definition)) {
441            return ctx;
442        }
443
444        ctx.setDefinition(new NodeDefinition(definition));
445
446        return ctx;
447    }
448
449    static class LineContext {
450        NodeDefinition definition;
451
452        int level;
453
454        public NodeDefinition getDefinition() {
455            return definition;
456        }
457
458        public void setDefinition(NodeDefinition definition) {
459            this.definition = definition;
460        }
461
462        public int getLevel() {
463            return level;
464        }
465
466        public void setLevel(int level) {
467            this.level = level;
468        }
469    }
470
471    public Collection<String> getSubstitutions() {
472        return substitutions;
473    }
474
475    public void setSubstitutions(Collection<String> substitutions) {
476        this.substitutions = substitutions;
477    }
478
479    public void setSubstitutions(String... substitutions) {
480        setSubstitutions(Arrays.asList(substitutions));
481    }
482}