org.eclipse.aether.internal.test.util.DependencyGraphParser Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.eclipse.aether.internal.test.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
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.artifact.DefaultArtifact;
import org.eclipse.aether.graph.DefaultDependencyNode;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.version.InvalidVersionSpecificationException;
import org.eclipse.aether.version.VersionScheme;
/**
* Creates a dependency graph from a text description. Definition
Each (non-empty) line in the input defines
* one node of the resulting graph:
*
*
* line ::= (indent? ("(null)" | node | reference))? comment?
* comment ::= "#" rest-of-line
* indent ::= "| "* ("+" | "\\") "- "
* reference ::= "^" id
* node ::= coords (range)? space (scope("<" premanagedScope)?)? space "optional"? space
* ("relocations=" coords ("," coords)*)? ("(" id ")")?
* coords ::= groupId ":" artifactId (":" extension (":" classifier)?)? ":" version
*
*
* The special token {@code (null)} may be used to indicate an "empty" root node with no dependency.
*
* If {@code indent} 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 of indentation.
*
* The {@code ^id} syntax allows to reuse a previously built node to share common sub graphs among different parent
* nodes.
*
Example
*
*
* gid:aid:ver
* +- gid:aid2:ver scope
* | \- gid:aid3:ver (id1) # assign id for reference below
* +- gid:aid4:ext:ver scope
* \- ^id1 # reuse previous node
*
*
* Multiple definitions in one resource
*
* By using {@link #parseMultiResource(String)}, definitions divided by a line beginning with "---" can be read from the
* same resource. The rest of the line is ignored.
*
Substitutions
*
* 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.
*
Example
*
*
* parser.setSubstitutions( "foo", "bar" );
* String def = "gid:%s:ext:ver\n" + "+- gid:%s:ext:ver";
*
*
* 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 final VersionScheme versionScheme;
private final String prefix;
private Collection substitutions;
/**
* Create a parser with the given prefix and the given substitution strings.
*
* @see DependencyGraphParser#parseResource(String)
*/
public DependencyGraphParser(String prefix, Collection substitutions) {
this.prefix = prefix;
this.substitutions = substitutions;
versionScheme = new TestVersionScheme();
}
/**
* Create a parser with the given prefix.
*
* @see DependencyGraphParser#parseResource(String)
*/
public DependencyGraphParser(String prefix) {
this(prefix, Collections.emptyList());
}
/**
* Create a parser with an empty prefix.
*/
public DependencyGraphParser() {
this("");
}
/**
* 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;
}
/**
* Parse the graph definition read from the given classpath resource. If a prefix is set, this method will load the
* resource from 'prefix + resource'.
*/
public DependencyNode parseResource(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 parseMultiResource(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(), StandardCharsets.UTF_8));
List ret = new ArrayList<>();
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 {
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8));
return parse(reader);
} finally {
try {
if (reader != null) {
reader.close();
reader = null;
}
} catch (final IOException e) {
// Suppressed due to an exception already thrown in the try block.
}
}
}
private DependencyNode parse(BufferedReader in) throws IOException {
Iterator substitutionIterator = (substitutions != null) ? substitutions.iterator() : null;
String line = null;
DependencyNode root = null;
DependencyNode node = null;
int prevLevel = 0;
Map nodes = new HashMap<>();
LinkedList stack = new LinkedList<>();
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 IllegalStateException("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().reference != null) {
String reference = ctx.getDefinition().reference;
DependencyNode child = nodes.get(reference);
if (child == null) {
throw new IllegalStateException("undefined reference " + reference);
}
node.getChildren().add(child);
} else {
node = build(isRootNode ? null : stack.getLast(), ctx, isRootNode);
if (isRootNode) {
root = node;
isRootNode = false;
}
if (ctx.getDefinition() != null && ctx.getDefinition().id != null) {
nodes.put(ctx.getDefinition().id, node);
}
}
}
return root;
}
private boolean isEOFMarker(String line) {
return line.startsWith("---");
}
private static boolean isEmpty(String line) {
return line == null || line.isEmpty();
}
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) {
NodeDefinition def = ctx.getDefinition();
if (!isRoot && parent == null) {
throw new IllegalStateException("dangling node: " + def);
} else if (ctx.getLevel() == 0 && parent != null) {
throw new IllegalStateException("inconsistent leveling (parent for level 0?): " + def);
}
DefaultDependencyNode node;
if (def != null) {
DefaultArtifact artifact = new DefaultArtifact(def.coords, def.properties);
Dependency dependency = new Dependency(artifact, def.scope, def.optional);
node = new DefaultDependencyNode(dependency);
int managedBits = 0;
if (def.premanagedScope != null) {
managedBits |= DependencyNode.MANAGED_SCOPE;
node.setData("premanaged.scope", def.premanagedScope);
}
if (def.premanagedVersion != null) {
managedBits |= DependencyNode.MANAGED_VERSION;
node.setData("premanaged.version", def.premanagedVersion);
}
node.setManagedBits(managedBits);
if (def.relocations != null) {
List relocations = new ArrayList<>();
for (String relocation : def.relocations) {
relocations.add(new DefaultArtifact(relocation));
}
node.setRelocations(relocations);
}
try {
node.setVersion(versionScheme.parseVersion(artifact.getVersion()));
node.setVersionConstraint(
versionScheme.parseVersionConstraint(def.range != null ? def.range : artifact.getVersion()));
} catch (InvalidVersionSpecificationException e) {
throw new IllegalArgumentException("bad version: " + e.getMessage(), e);
}
} else {
node = new DefaultDependencyNode((Dependency) null);
}
if (parent != null) {
parent.getChildren().add(node);
}
return node;
}
public String dump(DependencyNode root) {
StringBuilder ret = new StringBuilder();
List entries = new ArrayList<>();
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 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 properties = artifact.getProperties();
if (!(properties == null || properties.isEmpty())) {
for (Map.Entry 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 properties;
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
public String getDefinition() {
return definition;
}
public void setDefinition(String definition) {
this.definition = definition;
}
public Map getProperties() {
return properties;
}
public void setProperties(Map 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() / (double) 3));
definition = split[1];
}
if ("(null)".equalsIgnoreCase(definition)) {
return ctx;
}
ctx.setDefinition(new NodeDefinition(definition));
return ctx;
}
static class LineContext {
NodeDefinition definition;
int level;
public NodeDefinition getDefinition() {
return definition;
}
public void setDefinition(NodeDefinition definition) {
this.definition = definition;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
}
public Collection getSubstitutions() {
return substitutions;
}
public void setSubstitutions(Collection substitutions) {
this.substitutions = substitutions;
}
public void setSubstitutions(String... substitutions) {
setSubstitutions(Arrays.asList(substitutions));
}
}