View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.internal.test.util;
20  
21  import java.io.BufferedReader;
22  import java.io.IOException;
23  import java.io.InputStreamReader;
24  import java.io.StringReader;
25  import java.net.URL;
26  import java.nio.charset.StandardCharsets;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.Collection;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.Iterator;
33  import java.util.LinkedList;
34  import java.util.List;
35  import java.util.Map;
36  
37  import org.eclipse.aether.artifact.Artifact;
38  import org.eclipse.aether.artifact.DefaultArtifact;
39  import org.eclipse.aether.graph.DefaultDependencyNode;
40  import org.eclipse.aether.graph.Dependency;
41  import org.eclipse.aether.graph.DependencyNode;
42  import org.eclipse.aether.version.InvalidVersionSpecificationException;
43  import org.eclipse.aether.version.VersionScheme;
44  
45  /**
46   * Creates a dependency graph from a text description. <h2>Definition</h2> Each (non-empty) line in the input defines
47   * one node of the resulting graph:
48   *
49   * <pre>
50   * line      ::= (indent? ("(null)" | node | reference))? comment?
51   * comment   ::= "#" rest-of-line
52   * indent    ::= "|  "*  ("+" | "\\") "- "
53   * reference ::= "^" id
54   * node      ::= coords (range)? space (scope("&lt;" premanagedScope)?)? space "optional"? space
55   *                  ("relocations=" coords ("," coords)*)? ("(" id ")")?
56   * coords    ::= groupId ":" artifactId (":" extension (":" classifier)?)? ":" version
57   * </pre>
58   *
59   * The special token {@code (null)} may be used to indicate an "empty" root node with no dependency.
60   * <p>
61   * If {@code indent} is empty, the line defines the root node. Only one root node may be defined. The level is
62   * calculated by the distance from the beginning of the line. One level is three characters of indentation.
63   * <p>
64   * The {@code ^id} syntax allows to reuse a previously built node to share common sub graphs among different parent
65   * nodes.
66   * <h2>Example</h2>
67   *
68   * <pre>
69   * gid:aid:ver
70   * +- gid:aid2:ver scope
71   * |  \- gid:aid3:ver        (id1)    # assign id for reference below
72   * +- gid:aid4:ext:ver scope
73   * \- ^id1                            # reuse previous node
74   * </pre>
75   *
76   * <h2>Multiple definitions in one resource</h2>
77   * <p>
78   * By using {@link #parseMultiResource(String)}, definitions divided by a line beginning with "---" can be read from the
79   * same resource. The rest of the line is ignored.
80   * <h2>Substitutions</h2>
81   * <p>
82   * You may define substitutions (see {@link #setSubstitutions(String...)},
83   * {@link #DependencyGraphParser(String, Collection)}). Every '%s' in the definition will be substituted by the next
84   * String in the defined substitutions.
85   * <h3>Example</h3>
86   *
87   * <pre>
88   * parser.setSubstitutions( &quot;foo&quot;, &quot;bar&quot; );
89   * String def = &quot;gid:%s:ext:ver\n&quot; + &quot;+- gid:%s:ext:ver&quot;;
90   * </pre>
91   *
92   * The first node will have "foo" as its artifact id, the second node (child to the first) will have "bar" as its
93   * artifact id.
94   */
95  public class DependencyGraphParser {
96  
97      private final VersionScheme versionScheme;
98  
99      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         try (BufferedReader reader = new BufferedReader(new StringReader(dependencyGraph))) {
135             return parse(reader);
136         }
137     }
138 
139     /**
140      * Parse the graph definition read from the given classpath resource. If a prefix is set, this method will load the
141      * resource from 'prefix + resource'.
142      */
143     public DependencyNode parseResource(String resource) throws IOException {
144         URL res = this.getClass().getClassLoader().getResource(prefix + resource);
145         if (res == null) {
146             throw new IOException("Could not find classpath resource " + prefix + resource);
147         }
148         return parse(res);
149     }
150 
151     /**
152      * Parse multiple graphs in one resource, divided by "---".
153      */
154     public List<DependencyNode> parseMultiResource(String resource) throws IOException {
155         URL res = this.getClass().getClassLoader().getResource(prefix + resource);
156         if (res == null) {
157             throw new IOException("Could not find classpath resource " + prefix + resource);
158         }
159 
160         try (BufferedReader reader =
161                 new BufferedReader(new InputStreamReader(res.openStream(), StandardCharsets.UTF_8))) {
162             List<DependencyNode> ret = new ArrayList<>();
163             DependencyNode root = null;
164             while ((root = parse(reader)) != null) {
165                 ret.add(root);
166             }
167             return ret;
168         }
169     }
170 
171     /**
172      * Parse the graph definition read from the given URL.
173      */
174     public DependencyNode parse(URL resource) throws IOException {
175         try (BufferedReader reader =
176                 new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8))) {
177             return parse(reader);
178         }
179     }
180 
181     private DependencyNode parse(BufferedReader in) throws IOException {
182         Iterator<String> substitutionIterator = (substitutions != null) ? substitutions.iterator() : null;
183 
184         String line = null;
185 
186         DependencyNode root = null;
187         DependencyNode node = null;
188         int prevLevel = 0;
189 
190         Map<String, DependencyNode> nodes = new HashMap<>();
191         LinkedList<DependencyNode> stack = new LinkedList<>();
192         boolean isRootNode = true;
193 
194         while ((line = in.readLine()) != null) {
195             line = cutComment(line);
196 
197             if (isEmpty(line)) {
198                 // skip empty line
199                 continue;
200             }
201 
202             if (isEOFMarker(line)) {
203                 // stop parsing
204                 break;
205             }
206 
207             while (line.contains("%s")) {
208                 if (!substitutionIterator.hasNext()) {
209                     throw new IllegalStateException("not enough substitutions to fill placeholders");
210                 }
211                 line = line.replaceFirst("%s", substitutionIterator.next());
212             }
213 
214             LineContext ctx = createContext(line);
215             if (prevLevel < ctx.getLevel()) {
216                 // previous node is new parent
217                 stack.add(node);
218             }
219 
220             // get to real parent
221             while (prevLevel > ctx.getLevel()) {
222                 stack.removeLast();
223                 prevLevel -= 1;
224             }
225 
226             prevLevel = ctx.getLevel();
227 
228             if (ctx.getDefinition() != null && ctx.getDefinition().reference != null) {
229                 String reference = ctx.getDefinition().reference;
230                 DependencyNode child = nodes.get(reference);
231                 if (child == null) {
232                     throw new IllegalStateException("undefined reference " + reference);
233                 }
234                 node.getChildren().add(child);
235             } else {
236 
237                 node = build(isRootNode ? null : stack.getLast(), ctx, isRootNode);
238 
239                 if (isRootNode) {
240                     root = node;
241                     isRootNode = false;
242                 }
243 
244                 if (ctx.getDefinition() != null && ctx.getDefinition().id != null) {
245                     nodes.put(ctx.getDefinition().id, node);
246                 }
247             }
248         }
249 
250         return root;
251     }
252 
253     private boolean isEOFMarker(String line) {
254         return line.startsWith("---");
255     }
256 
257     private static boolean isEmpty(String line) {
258         return line == null || line.isEmpty();
259     }
260 
261     private static String cutComment(String line) {
262         int idx = line.indexOf('#');
263 
264         if (idx != -1) {
265             line = line.substring(0, idx);
266         }
267 
268         return line;
269     }
270 
271     private DependencyNode build(DependencyNode parent, LineContext ctx, boolean isRoot) {
272         NodeDefinition def = ctx.getDefinition();
273         if (!isRoot && parent == null) {
274             throw new IllegalStateException("dangling node: " + def);
275         } else if (ctx.getLevel() == 0 && parent != null) {
276             throw new IllegalStateException("inconsistent leveling (parent for level 0?): " + def);
277         }
278 
279         DefaultDependencyNode node;
280         if (def != null) {
281             DefaultArtifact artifact = new DefaultArtifact(def.coords, def.properties);
282             Dependency dependency = new Dependency(artifact, def.scope, def.optional);
283             node = new DefaultDependencyNode(dependency);
284             int managedBits = 0;
285             if (def.premanagedScope != null) {
286                 managedBits |= DependencyNode.MANAGED_SCOPE;
287                 node.setData("premanaged.scope", def.premanagedScope);
288             }
289             if (def.premanagedVersion != null) {
290                 managedBits |= DependencyNode.MANAGED_VERSION;
291                 node.setData("premanaged.version", def.premanagedVersion);
292             }
293             node.setManagedBits(managedBits);
294             if (def.relocations != null) {
295                 List<Artifact> relocations = new ArrayList<>();
296                 for (String relocation : def.relocations) {
297                     relocations.add(new DefaultArtifact(relocation));
298                 }
299                 node.setRelocations(relocations);
300             }
301             try {
302                 node.setVersion(versionScheme.parseVersion(artifact.getVersion()));
303                 node.setVersionConstraint(
304                         versionScheme.parseVersionConstraint(def.range != null ? def.range : artifact.getVersion()));
305             } catch (InvalidVersionSpecificationException e) {
306                 throw new IllegalArgumentException("bad version: " + e.getMessage(), e);
307             }
308         } else {
309             node = new DefaultDependencyNode((Dependency) null);
310         }
311 
312         if (parent != null) {
313             parent.getChildren().add(node);
314         }
315 
316         return node;
317     }
318 
319     public String dump(DependencyNode root) {
320         StringBuilder ret = new StringBuilder();
321 
322         List<NodeEntry> entries = new ArrayList<>();
323 
324         addNode(root, 0, entries);
325 
326         for (NodeEntry nodeEntry : entries) {
327             char[] level = new char[(nodeEntry.getLevel() * 3)];
328             Arrays.fill(level, ' ');
329 
330             if (level.length != 0) {
331                 level[level.length - 3] = '+';
332                 level[level.length - 2] = '-';
333             }
334 
335             String definition = nodeEntry.getDefinition();
336 
337             ret.append(level).append(definition).append("\n");
338         }
339 
340         return ret.toString();
341     }
342 
343     private void addNode(DependencyNode root, int level, List<NodeEntry> entries) {
344 
345         NodeEntry entry = new NodeEntry();
346         Dependency dependency = root.getDependency();
347         StringBuilder defBuilder = new StringBuilder();
348         if (dependency == null) {
349             defBuilder.append("(null)");
350         } else {
351             Artifact artifact = dependency.getArtifact();
352 
353             defBuilder
354                     .append(artifact.getGroupId())
355                     .append(":")
356                     .append(artifact.getArtifactId())
357                     .append(":")
358                     .append(artifact.getExtension())
359                     .append(":")
360                     .append(artifact.getVersion());
361             if (dependency.getScope() != null && (!"".equals(dependency.getScope()))) {
362                 defBuilder.append(":").append(dependency.getScope());
363             }
364 
365             Map<String, String> properties = artifact.getProperties();
366             if (!(properties == null || properties.isEmpty())) {
367                 for (Map.Entry<String, String> prop : properties.entrySet()) {
368                     defBuilder.append(";").append(prop.getKey()).append("=").append(prop.getValue());
369                 }
370             }
371         }
372 
373         entry.setDefinition(defBuilder.toString());
374         entry.setLevel(level++);
375 
376         entries.add(entry);
377 
378         for (DependencyNode node : root.getChildren()) {
379             addNode(node, level, entries);
380         }
381     }
382 
383     private static class NodeEntry {
384         int level;
385 
386         String definition;
387 
388         Map<String, String> properties;
389 
390         public int getLevel() {
391             return level;
392         }
393 
394         public void setLevel(int level) {
395             this.level = level;
396         }
397 
398         public String getDefinition() {
399             return definition;
400         }
401 
402         public void setDefinition(String definition) {
403             this.definition = definition;
404         }
405 
406         public Map<String, String> getProperties() {
407             return properties;
408         }
409 
410         public void setProperties(Map<String, String> properties) {
411             this.properties = properties;
412         }
413     }
414 
415     private static LineContext createContext(String line) {
416         LineContext ctx = new LineContext();
417         String definition;
418 
419         String[] split = line.split("- ");
420         if (split.length == 1) // root
421         {
422             ctx.setLevel(0);
423             definition = split[0];
424         } else {
425             ctx.setLevel((int) Math.ceil((double) split[0].length() / (double) 3));
426             definition = split[1];
427         }
428 
429         if ("(null)".equalsIgnoreCase(definition)) {
430             return ctx;
431         }
432 
433         ctx.setDefinition(new NodeDefinition(definition));
434 
435         return ctx;
436     }
437 
438     static class LineContext {
439         NodeDefinition definition;
440 
441         int level;
442 
443         public NodeDefinition getDefinition() {
444             return definition;
445         }
446 
447         public void setDefinition(NodeDefinition definition) {
448             this.definition = definition;
449         }
450 
451         public int getLevel() {
452             return level;
453         }
454 
455         public void setLevel(int level) {
456             this.level = level;
457         }
458     }
459 
460     public Collection<String> getSubstitutions() {
461         return substitutions;
462     }
463 
464     public void setSubstitutions(Collection<String> substitutions) {
465         this.substitutions = substitutions;
466     }
467 
468     public void setSubstitutions(String... substitutions) {
469         setSubstitutions(Arrays.asList(substitutions));
470     }
471 }