Add provides API (#1853)
Add provides API Co-authored-by: Shane Freeder <theboyetronic@gmail.com>
This commit is contained in:
@@ -97,6 +97,16 @@ public class PluginAnnotationProcessor extends AbstractProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
for (String provided : plugin.provides()) {
|
||||
if (!SerializedPluginDescription.ID_PATTERN.matcher(provided).matches()) {
|
||||
environment.getMessager().printMessage(Diagnostic.Kind.ERROR,
|
||||
"Invalid provided ID '" + provided + "' for plugin " + qualifiedName
|
||||
+ ". IDs must start alphabetically, have lowercase alphanumeric characters, and "
|
||||
+ "can contain dashes or underscores.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// All good, generate the velocity-plugin.json.
|
||||
SerializedPluginDescription description = SerializedPluginDescription
|
||||
.from(plugin, qualifiedName.toString());
|
||||
|
||||
@@ -35,11 +35,12 @@ public final class SerializedPluginDescription {
|
||||
private final @Nullable String url;
|
||||
private final @Nullable List<String> authors;
|
||||
private final @Nullable List<Dependency> dependencies;
|
||||
private final @Nullable List<String> provides;
|
||||
private final String main;
|
||||
|
||||
private SerializedPluginDescription(String id, String name, String version, String description,
|
||||
String url,
|
||||
List<String> authors, List<Dependency> dependencies, String main) {
|
||||
List<String> authors, List<Dependency> dependencies, List<String> provides, String main) {
|
||||
Preconditions.checkNotNull(id, "id");
|
||||
Preconditions.checkArgument(ID_PATTERN.matcher(id).matches(), "id is not valid");
|
||||
this.id = id;
|
||||
@@ -50,6 +51,7 @@ public final class SerializedPluginDescription {
|
||||
this.authors = authors == null || authors.isEmpty() ? ImmutableList.of() : authors;
|
||||
this.dependencies =
|
||||
dependencies == null || dependencies.isEmpty() ? ImmutableList.of() : dependencies;
|
||||
this.provides = provides == null || provides.isEmpty() ? ImmutableList.of() : provides;
|
||||
this.main = Preconditions.checkNotNull(main, "main");
|
||||
}
|
||||
|
||||
@@ -61,7 +63,9 @@ public final class SerializedPluginDescription {
|
||||
return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(),
|
||||
plugin.description(), plugin.url(),
|
||||
Arrays.stream(plugin.authors()).filter(author -> !author.isEmpty())
|
||||
.collect(Collectors.toList()), dependencies, qualifiedName);
|
||||
.collect(Collectors.toList()), dependencies,
|
||||
Arrays.stream(plugin.provides()).filter(provided -> !provided.isEmpty())
|
||||
.collect(Collectors.toList()), qualifiedName);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
@@ -92,6 +96,10 @@ public final class SerializedPluginDescription {
|
||||
return dependencies == null ? ImmutableList.of() : dependencies;
|
||||
}
|
||||
|
||||
public List<String> getProvides() {
|
||||
return provides == null ? ImmutableList.of() : provides;
|
||||
}
|
||||
|
||||
public String getMain() {
|
||||
return main;
|
||||
}
|
||||
@@ -112,12 +120,13 @@ public final class SerializedPluginDescription {
|
||||
&& Objects.equals(url, that.url)
|
||||
&& Objects.equals(authors, that.authors)
|
||||
&& Objects.equals(dependencies, that.dependencies)
|
||||
&& Objects.equals(provides, that.provides)
|
||||
&& Objects.equals(main, that.main);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, name, version, description, url, authors, dependencies);
|
||||
return Objects.hash(id, name, version, description, url, authors, dependencies, provides);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -130,6 +139,7 @@ public final class SerializedPluginDescription {
|
||||
+ ", url='" + url + '\''
|
||||
+ ", authors=" + authors
|
||||
+ ", dependencies=" + dependencies
|
||||
+ ", provides=" + provides
|
||||
+ ", main='" + main + '\''
|
||||
+ '}';
|
||||
}
|
||||
|
||||
@@ -72,4 +72,12 @@ public @interface Plugin {
|
||||
* @return the plugin dependencies
|
||||
*/
|
||||
Dependency[] dependencies() default {};
|
||||
|
||||
/**
|
||||
* The plugin IDs this plugin "provides" for. Each ID must match
|
||||
* {@link SerializedPluginDescription#ID_PATTERN_STRING}.
|
||||
*
|
||||
* @return the provided IDs
|
||||
*/
|
||||
String[] provides() default {};
|
||||
}
|
||||
|
||||
@@ -100,6 +100,16 @@ public interface PluginDescription {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a {@link Collection} of the provided IDs of the {@link Plugin} within this container.
|
||||
*
|
||||
* @return the provided plugins IDs, can be empty
|
||||
* @see Plugin#provides()
|
||||
*/
|
||||
default Collection<String> getProvidedIds() {
|
||||
return ImmutableSet.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source the plugin was loaded from.
|
||||
*
|
||||
|
||||
@@ -221,7 +221,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
||||
PluginDescription description = new VelocityPluginDescription(
|
||||
"velocity", version.getName(), version.getVersion(), "The Velocity proxy",
|
||||
version.getName().equals("Velocity") ? VELOCITY_URL : null,
|
||||
ImmutableList.of(version.getVendor()), Collections.emptyList(), null);
|
||||
ImmutableList.of(version.getVendor()), Collections.emptyList(),
|
||||
Collections.emptyList(), null);
|
||||
VelocityPluginContainer container = new VelocityPluginContainer(description);
|
||||
container.setInstance(VelocityVirtualPlugin.INSTANCE);
|
||||
return container;
|
||||
|
||||
@@ -46,10 +46,12 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
@@ -62,6 +64,7 @@ public class VelocityPluginManager implements PluginManager {
|
||||
|
||||
private final Map<String, PluginContainer> pluginsById = new LinkedHashMap<>();
|
||||
private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>();
|
||||
private final Set<PluginContainer> plugins = new LinkedHashSet<>();
|
||||
private final VelocityServer server;
|
||||
|
||||
public VelocityPluginManager(VelocityServer server) {
|
||||
@@ -74,7 +77,9 @@ public class VelocityPluginManager implements PluginManager {
|
||||
* @param plugin the plugin to register
|
||||
*/
|
||||
public void registerPlugin(PluginContainer plugin) {
|
||||
plugins.add(plugin);
|
||||
pluginsById.put(plugin.getDescription().getId(), plugin);
|
||||
plugin.getDescription().getProvidedIds().forEach(id -> pluginsById.put(id, plugin));
|
||||
Optional<?> instance = plugin.getInstance();
|
||||
instance.ifPresent(o -> pluginInstances.put(o, plugin));
|
||||
}
|
||||
@@ -100,16 +105,34 @@ public class VelocityPluginManager implements PluginManager {
|
||||
try {
|
||||
PluginDescription candidate = loader.loadCandidate(path);
|
||||
|
||||
// If we found a duplicate candidate (with the same ID), don't load it.
|
||||
PluginDescription maybeExistingCandidate = foundCandidates.putIfAbsent(
|
||||
candidate.getId(), candidate);
|
||||
// A plugin claims its own ID plus every ID it provides. If any of those are already
|
||||
// claimed by another candidate, don't load this one.
|
||||
List<String> claimedIds = new ArrayList<>(candidate.getProvidedIds().size() + 1);
|
||||
claimedIds.add(candidate.getId());
|
||||
claimedIds.addAll(candidate.getProvidedIds());
|
||||
|
||||
if (maybeExistingCandidate != null) {
|
||||
logger.error("Refusing to load plugin at path {} since we already "
|
||||
+ "loaded a plugin with the same ID {} from {}",
|
||||
PluginDescription conflict = null;
|
||||
String conflictingId = null;
|
||||
for (String id : claimedIds) {
|
||||
PluginDescription existing = foundCandidates.get(id);
|
||||
if (existing != null) {
|
||||
conflict = existing;
|
||||
conflictingId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (conflict != null) {
|
||||
logger.error("Refusing to load plugin at path {} since ID {} was already "
|
||||
+ "claimed by a plugin loaded from {}",
|
||||
candidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"),
|
||||
candidate.getId(),
|
||||
maybeExistingCandidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"));
|
||||
conflictingId,
|
||||
conflict.getSource().map(Objects::toString).orElse("<UNKNOWN>"));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (String id : claimedIds) {
|
||||
foundCandidates.put(id, candidate);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
logger.error("Unable to load plugin {}", path, e);
|
||||
@@ -122,8 +145,10 @@ public class VelocityPluginManager implements PluginManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// foundCandidates indexes each candidate under its ID and any provided IDs, so dedupe before
|
||||
// sorting to avoid loading a plugin more than once.
|
||||
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(
|
||||
new ArrayList<>(foundCandidates.values()));
|
||||
new ArrayList<>(new LinkedHashSet<>(foundCandidates.values())));
|
||||
|
||||
Map<String, PluginDescription> loadedCandidates = new HashMap<>();
|
||||
Map<PluginContainer, Module> pluginContainers = new LinkedHashMap<>();
|
||||
@@ -144,6 +169,7 @@ public class VelocityPluginManager implements PluginManager {
|
||||
VelocityPluginContainer container = new VelocityPluginContainer(realPlugin);
|
||||
pluginContainers.put(container, loader.createModule(container));
|
||||
loadedCandidates.put(realPlugin.getId(), realPlugin);
|
||||
realPlugin.getProvidedIds().forEach(id -> loadedCandidates.putIfAbsent(id, realPlugin));
|
||||
} catch (Throwable e) {
|
||||
logger.error("Can't create module for plugin {}", candidate.getId(), e);
|
||||
}
|
||||
@@ -201,7 +227,7 @@ public class VelocityPluginManager implements PluginManager {
|
||||
|
||||
@Override
|
||||
public Collection<PluginContainer> getPlugins() {
|
||||
return Collections.unmodifiableCollection(pluginsById.values());
|
||||
return Collections.unmodifiableCollection(plugins);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+13
-1
@@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
@@ -43,6 +44,7 @@ public class VelocityPluginDescription implements PluginDescription {
|
||||
private final @Nullable String url;
|
||||
private final List<String> authors;
|
||||
private final Map<String, PluginDependency> dependencies;
|
||||
private final Collection<String> providedIds;
|
||||
private final Path source;
|
||||
|
||||
/**
|
||||
@@ -55,11 +57,13 @@ public class VelocityPluginDescription implements PluginDescription {
|
||||
* @param url the website for the plugin
|
||||
* @param authors the authors of this plugin
|
||||
* @param dependencies the dependencies for this plugin
|
||||
* @param providedIds the IDs this plugin provides for
|
||||
* @param source the original source for the plugin
|
||||
*/
|
||||
public VelocityPluginDescription(String id, @Nullable String name, @Nullable String version,
|
||||
@Nullable String description, @Nullable String url,
|
||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source) {
|
||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies,
|
||||
@Nullable Collection<String> providedIds, Path source) {
|
||||
this.id = checkNotNull(id, "id");
|
||||
this.name = Strings.emptyToNull(name);
|
||||
this.version = Strings.emptyToNull(version);
|
||||
@@ -67,6 +71,8 @@ public class VelocityPluginDescription implements PluginDescription {
|
||||
this.url = Strings.emptyToNull(url);
|
||||
this.authors = authors == null ? ImmutableList.of() : ImmutableList.copyOf(authors);
|
||||
this.dependencies = Maps.uniqueIndex(dependencies, d -> d == null ? null : d.getId());
|
||||
this.providedIds =
|
||||
providedIds == null ? ImmutableSet.of() : ImmutableSet.copyOf(providedIds);
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@@ -110,6 +116,11 @@ public class VelocityPluginDescription implements PluginDescription {
|
||||
return Optional.ofNullable(dependencies.get(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getProvidedIds() {
|
||||
return providedIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> getSource() {
|
||||
return Optional.ofNullable(source);
|
||||
@@ -125,6 +136,7 @@ public class VelocityPluginDescription implements PluginDescription {
|
||||
+ ", url='" + url + '\''
|
||||
+ ", authors=" + authors
|
||||
+ ", dependencies=" + dependencies
|
||||
+ ", providedIds=" + providedIds
|
||||
+ ", source=" + source
|
||||
+ '}';
|
||||
}
|
||||
|
||||
+10
@@ -78,6 +78,14 @@ public class JavaPluginLoader implements PluginLoader {
|
||||
}
|
||||
}
|
||||
|
||||
for (String providedId : pd.getProvides()) {
|
||||
if (!SerializedPluginDescription.ID_PATTERN.matcher(providedId).matches()) {
|
||||
throw new InvalidPluginException(
|
||||
"Provided ID '" + providedId + "' for plugin '" + pd.getId() + "' is invalid."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return createCandidateDescription(pd, source);
|
||||
}
|
||||
|
||||
@@ -181,6 +189,7 @@ public class JavaPluginLoader implements PluginLoader {
|
||||
description.getUrl(),
|
||||
description.getAuthors(),
|
||||
dependencies,
|
||||
description.getProvides(),
|
||||
source,
|
||||
description.getMain()
|
||||
);
|
||||
@@ -197,6 +206,7 @@ public class JavaPluginLoader implements PluginLoader {
|
||||
description.getUrl().orElse(null),
|
||||
description.getAuthors(),
|
||||
description.getDependencies(),
|
||||
description.getProvidedIds(),
|
||||
description.getSource().orElse(null),
|
||||
mainClass
|
||||
);
|
||||
|
||||
+3
-3
@@ -32,9 +32,9 @@ class JavaVelocityPluginDescription extends VelocityPluginDescription {
|
||||
|
||||
JavaVelocityPluginDescription(String id, @Nullable String name, @Nullable String version,
|
||||
@Nullable String description, @Nullable String url,
|
||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source,
|
||||
Class<?> mainClass) {
|
||||
super(id, name, version, description, url, authors, dependencies, source);
|
||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies,
|
||||
@Nullable Collection<String> providedIds, Path source, Class<?> mainClass) {
|
||||
super(id, name, version, description, url, authors, dependencies, providedIds, source);
|
||||
this.mainClass = checkNotNull(mainClass);
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -32,9 +32,9 @@ class JavaVelocityPluginDescriptionCandidate extends VelocityPluginDescription {
|
||||
|
||||
JavaVelocityPluginDescriptionCandidate(String id, @Nullable String name, @Nullable String version,
|
||||
@Nullable String description, @Nullable String url,
|
||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source,
|
||||
String mainClass) {
|
||||
super(id, name, version, description, url, authors, dependencies, source);
|
||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies,
|
||||
@Nullable Collection<String> providedIds, Path source, String mainClass) {
|
||||
super(id, name, version, description, url, authors, dependencies, providedIds, source);
|
||||
this.mainClass = checkNotNull(mainClass);
|
||||
}
|
||||
|
||||
|
||||
+15
-4
@@ -17,7 +17,6 @@
|
||||
|
||||
package com.velocitypowered.proxy.plugin.util;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.graph.Graph;
|
||||
import com.google.common.graph.GraphBuilder;
|
||||
import com.google.common.graph.MutableGraph;
|
||||
@@ -60,8 +59,19 @@ public class PluginDependencyUtils {
|
||||
.allowsSelfLoops(false)
|
||||
.expectedNodeCount(sortedCandidates.size())
|
||||
.build();
|
||||
Map<String, PluginDescription> candidateMap = Maps.uniqueIndex(sortedCandidates,
|
||||
PluginDescription::getId);
|
||||
|
||||
// Index candidates by their own ID and any IDs they provide, so a dependency can be satisfied
|
||||
// by a plugin that provides that ID. Real IDs take precedence over provided IDs, and the first
|
||||
// provider of a given ID wins; upstream loading rejects such conflicts before we get here.
|
||||
Map<String, PluginDescription> candidateMap = new HashMap<>();
|
||||
for (PluginDescription description : sortedCandidates) {
|
||||
candidateMap.putIfAbsent(description.getId(), description);
|
||||
}
|
||||
for (PluginDescription description : sortedCandidates) {
|
||||
for (String provided : description.getProvidedIds()) {
|
||||
candidateMap.putIfAbsent(provided, description);
|
||||
}
|
||||
}
|
||||
|
||||
for (PluginDescription description : sortedCandidates) {
|
||||
graph.addNode(description);
|
||||
@@ -69,7 +79,8 @@ public class PluginDependencyUtils {
|
||||
for (PluginDependency dependency : description.getDependencies()) {
|
||||
PluginDescription in = candidateMap.get(dependency.getId());
|
||||
|
||||
if (in != null) {
|
||||
// Guard against self-loops: a plugin may name an ID it itself provides.
|
||||
if (in != null && !in.equals(description)) {
|
||||
graph.putEdge(description, in);
|
||||
}
|
||||
}
|
||||
|
||||
+31
-1
@@ -44,6 +44,15 @@ class PluginDependencyUtilsTest {
|
||||
private static final PluginDescription CIRCULAR_DEPENDENCY_2 = testDescription("oval",
|
||||
new PluginDependency("circle", "", false));
|
||||
|
||||
// "provider" is loaded from a real ID but provides the virtual ID "some-api"; "consumer" and
|
||||
// "zdependent" depend on it only through that provided ID / a chain that reaches it.
|
||||
private static final PluginDescription PROVIDES_API = providingDescription("provider",
|
||||
ImmutableList.of("some-api"));
|
||||
private static final PluginDescription DEPENDS_ON_PROVIDED = providingDescription("consumer",
|
||||
ImmutableList.of(), new PluginDependency("some-api", null, false));
|
||||
private static final PluginDescription DEPENDS_ON_CONSUMER = testDescription("zdependent",
|
||||
new PluginDependency("consumer", null, false));
|
||||
|
||||
@Test
|
||||
void sortCandidatesTrivial() throws Exception {
|
||||
List<PluginDescription> descriptionList = new ArrayList<>();
|
||||
@@ -96,10 +105,31 @@ class PluginDependencyUtilsTest {
|
||||
assertThrows(IllegalStateException.class, () -> PluginDependencyUtils.sortCandidates(descs));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortCandidatesResolvesProvidedDependency() throws Exception {
|
||||
List<PluginDescription> plugins = ImmutableList.of(DEPENDS_ON_PROVIDED, PROVIDES_API);
|
||||
List<PluginDescription> expected = ImmutableList.of(PROVIDES_API, DEPENDS_ON_PROVIDED);
|
||||
assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortCandidatesResolvesTransitiveProvidedDependency() throws Exception {
|
||||
List<PluginDescription> plugins = ImmutableList.of(DEPENDS_ON_CONSUMER, DEPENDS_ON_PROVIDED,
|
||||
PROVIDES_API);
|
||||
List<PluginDescription> expected = ImmutableList.of(PROVIDES_API, DEPENDS_ON_PROVIDED,
|
||||
DEPENDS_ON_CONSUMER);
|
||||
assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins));
|
||||
}
|
||||
|
||||
private static PluginDescription testDescription(String id, PluginDependency... dependencies) {
|
||||
return providingDescription(id, ImmutableList.of(), dependencies);
|
||||
}
|
||||
|
||||
private static PluginDescription providingDescription(String id, List<String> provides,
|
||||
PluginDependency... dependencies) {
|
||||
return new VelocityPluginDescription(
|
||||
id, "tuxed", "0.1", null, null, ImmutableList.of(),
|
||||
ImmutableList.copyOf(dependencies), null
|
||||
ImmutableList.copyOf(dependencies), provides, null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user