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.<String>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         BufferedReader reader = new BufferedReader(new InputStreamReader(res.openStream(), StandardCharsets.UTF_8));
162 
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      * Parse the graph definition read from the given URL.
173      */
174     public DependencyNode parse(URL resource) throws IOException {
175         BufferedReader reader = null;
176         try {
177             reader = new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8));
178             return parse(reader);
179         } finally {
180             try {
181                 if (reader != null) {
182                     reader.close();
183                     reader = null;
184                 }
185             } catch (final IOException e) {
186                 // Suppressed due to an exception already thrown in the try block.
187             }
188         }
189     }
190 
191     private DependencyNode parse(BufferedReader in) throws IOException {
192         Iterator<String> substitutionIterator = (substitutions != null) ? substitutions.iterator() : null;
193 
194         String line = null;
195 
196         DependencyNode root = null;
197         DependencyNode node = null;
198         int prevLevel = 0;
199 
200         Map<String, DependencyNode> nodes = new HashMap<>();
201         LinkedList<DependencyNode> stack = new LinkedList<>();
202         boolean isRootNode = true;
203 
204         while ((line = in.readLine()) != null) {
205             line = cutComment(line);
206 
207             if (isEmpty(line)) {
208                 // skip empty line
209                 continue;
210             }
211 
212             if (isEOFMarker(line)) {
213                 // stop parsing
214                 break;
215             }
216 
217             while (line.contains("%s")) {
218                 if (!substitutionIterator.hasNext()) {
219                     throw new IllegalStateException("not enough substitutions to fill placeholders");
220                 }
221                 line = line.replaceFirst("%s", substitutionIterator.next());
222             }
223 
224             LineContext ctx = createContext(line);
225             if (prevLevel < ctx.getLevel()) {
226                 // previous node is new parent
227                 stack.add(node);
228             }
229 
230             // get to real parent
231             while (prevLevel > ctx.getLevel()) {
232                 stack.removeLast();
233                 prevLevel -= 1;
234             }
235 
236             prevLevel = ctx.getLevel();
237 
238             if (ctx.getDefinition() != null && ctx.getDefinition().reference != null) {
239                 String reference = ctx.getDefinition().reference;
240                 DependencyNode child = nodes.get(reference);
241                 if (child == null) {
242                     throw new IllegalStateException("undefined reference " + reference);
243                 }
244                 node.getChildren().add(child);
245             } else {
246 
247                 node = build(isRootNode ? null : stack.getLast(), ctx, isRootNode);
248 
249                 if (isRootNode) {
250                     root = node;
251                     isRootNode = false;
252                 }
253 
254                 if (ctx.getDefinition() != null && ctx.getDefinition().id != null) {
255                     nodes.put(ctx.getDefinition().id, node);
256                 }
257             }
258         }
259 
260         return root;
261     }
262 
263     private boolean isEOFMarker(String line) {
264         return line.startsWith("---");
265     }
266 
267     private static boolean isEmpty(String line) {
268         return line == null || line.isEmpty();
269     }
270 
271     private static String cutComment(String line) {
272         int idx = line.indexOf('#');
273 
274         if (idx != -1) {
275             line = line.substring(0, idx);
276         }
277 
278         return line;
279     }
280 
281     private DependencyNode build(DependencyNode parent, LineContext ctx, boolean isRoot) {
282         NodeDefinition def = ctx.getDefinition();
283         if (!isRoot && parent == null) {
284             throw new IllegalStateException("dangling node: " + def);
285         } else if (ctx.getLevel() == 0 && parent != null) {
286             throw new IllegalStateException("inconsistent leveling (parent for level 0?): " + def);
287         }
288 
289         DefaultDependencyNode node;
290         if (def != null) {
291             DefaultArtifact artifact = new DefaultArtifact(def.coords, def.properties);
292             Dependency dependency = new Dependency(artifact, def.scope, def.optional);
293             node = new DefaultDependencyNode(dependency);
294             int managedBits = 0;
295             if (def.premanagedScope != null) {
296                 managedBits |= DependencyNode.MANAGED_SCOPE;
297                 node.setData("premanaged.scope", def.premanagedScope);
298             }
299             if (def.premanagedVersion != null) {
300                 managedBits |= DependencyNode.MANAGED_VERSION;
301                 node.setData("premanaged.version", def.premanagedVersion);
302             }
303             node.setManagedBits(managedBits);
304             if (def.relocations != null) {
305                 List<Artifact> relocations = new ArrayList<>();
306                 for (String relocation : def.relocations) {
307                     relocations.add(new DefaultArtifact(relocation));
308                 }
309                 node.setRelocations(relocations);
310             }
311             try {
312                 node.setVersion(versionScheme.parseVersion(artifact.getVersion()));
313                 node.setVersionConstraint(
314                         versionScheme.parseVersionConstraint(def.range != null ? def.range : artifact.getVersion()));
315             } catch (InvalidVersionSpecificationException e) {
316                 throw new IllegalArgumentException("bad version: " + e.getMessage(), e);
317             }
318         } else {
319             node = new DefaultDependencyNode((Dependency) null);
320         }
321 
322         if (parent != null) {
323             parent.getChildren().add(node);
324         }
325 
326         return node;
327     }
328 
329     public String dump(DependencyNode root) {
330         StringBuilder ret = new StringBuilder();
331 
332         List<NodeEntry> entries = new ArrayList<>();
333 
334         addNode(root, 0, entries);
335 
336         for (NodeEntry nodeEntry : entries) {
337             char[] level = new char[(nodeEntry.getLevel() * 3)];
338             Arrays.fill(level, ' ');
339 
340             if (level.length != 0) {
341                 level[level.length - 3] = '+';
342                 level[level.length - 2] = '-';
343             }
344 
345             String definition = nodeEntry.getDefinition();
346 
347             ret.append(level).append(definition).append("\n");
348         }
349 
350         return ret.toString();
351     }
352 
353     private void addNode(DependencyNode root, int level, List<NodeEntry> entries) {
354 
355         NodeEntry entry = new NodeEntry();
356         Dependency dependency = root.getDependency();
357         StringBuilder defBuilder = new StringBuilder();
358         if (dependency == null) {
359             defBuilder.append("(null)");
360         } else {
361             Artifact artifact = dependency.getArtifact();
362 
363             defBuilder
364                     .append(artifact.getGroupId())
365                     .append(":")
366                     .append(artifact.getArtifactId())
367                     .append(":")
368                     .append(artifact.getExtension())
369                     .append(":")
370                     .append(artifact.getVersion());
371             if (dependency.getScope() != null && (!"".equals(dependency.getScope()))) {
372                 defBuilder.append(":").append(dependency.getScope());
373             }
374 
375             Map<String, String> properties = artifact.getProperties();
376             if (!(properties == null || properties.isEmpty())) {
377                 for (Map.Entry<String, String> prop : properties.entrySet()) {
378                     defBuilder.append(";").append(prop.getKey()).append("=").append(prop.getValue());
379                 }
380             }
381         }
382 
383         entry.setDefinition(defBuilder.toString());
384         entry.setLevel(level++);
385 
386         entries.add(entry);
387 
388         for (DependencyNode node : root.getChildren()) {
389             addNode(node, level, entries);
390         }
391     }
392 
393     class NodeEntry {
394         int level;
395 
396         String definition;
397 
398         Map<String, String> properties;
399 
400         public int getLevel() {
401             return level;
402         }
403 
404         public void setLevel(int level) {
405             this.level = level;
406         }
407 
408         public String getDefinition() {
409             return definition;
410         }
411 
412         public void setDefinition(String definition) {
413             this.definition = definition;
414         }
415 
416         public Map<String, String> getProperties() {
417             return properties;
418         }
419 
420         public void setProperties(Map<String, String> properties) {
421             this.properties = properties;
422         }
423     }
424 
425     private static LineContext createContext(String line) {
426         LineContext ctx = new LineContext();
427         String definition;
428 
429         String[] split = line.split("- ");
430         if (split.length == 1) // root
431         {
432             ctx.setLevel(0);
433             definition = split[0];
434         } else {
435             ctx.setLevel((int) Math.ceil((double) split[0].length() / (double) 3));
436             definition = split[1];
437         }
438 
439         if ("(null)".equalsIgnoreCase(definition)) {
440             return ctx;
441         }
442 
443         ctx.setDefinition(new NodeDefinition(definition));
444 
445         return ctx;
446     }
447 
448     static class LineContext {
449         NodeDefinition definition;
450 
451         int level;
452 
453         public NodeDefinition getDefinition() {
454             return definition;
455         }
456 
457         public void setDefinition(NodeDefinition definition) {
458             this.definition = definition;
459         }
460 
461         public int getLevel() {
462             return level;
463         }
464 
465         public void setLevel(int level) {
466             this.level = level;
467         }
468     }
469 
470     public Collection<String> getSubstitutions() {
471         return substitutions;
472     }
473 
474     public void setSubstitutions(Collection<String> substitutions) {
475         this.substitutions = substitutions;
476     }
477 
478     public void setSubstitutions(String... substitutions) {
479         setSubstitutions(Arrays.asList(substitutions));
480     }
481 }