Skip to content

Commit

Permalink
Merge pull request #5388 from bndtools/issue/5368
Browse files Browse the repository at this point in the history
[external plugins] Added versioning support
  • Loading branch information
pkriens committed Oct 6, 2022
2 parents 6106191 + ed4db27 commit 8762af5
Show file tree
Hide file tree
Showing 12 changed files with 213 additions and 35 deletions.
2 changes: 1 addition & 1 deletion biz.aQute.bndall.tests/plugin-2.bnd
Expand Up @@ -2,4 +2,4 @@

Main-Class biz.aQute.bndall.tests.plugin_2.MainClass

Provide-Capability bnd.mainclass; bnd.mainclass=biz.aQute.bndall.tests.plugin_2.MainClass
Bundle-Version: 1.2.3
Expand Up @@ -9,7 +9,7 @@
import aQute.bnd.service.externalplugin.ExternalPlugin;
import aQute.service.reporter.Reporter;

@ExternalPlugin(name = "hellocallable", objectClass = Callable.class)
@ExternalPlugin(name = "hellocallable", objectClass = Callable.class, version = "1.0.0")
public class CallablePlugin implements Callable<String>, Closeable, Plugin {
boolean closed;
String world = "world";
Expand Down
@@ -0,0 +1,41 @@
package biz.aQute.bndall.tests.plugin_1;

import java.io.Closeable;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.Callable;

import aQute.bnd.service.Plugin;
import aQute.bnd.service.externalplugin.ExternalPlugin;
import aQute.service.reporter.Reporter;

@ExternalPlugin(name = "hellocallable", objectClass = Callable.class, version = "2.0.0")
public class CallablePlugin2 implements Callable<String>, Closeable, Plugin {
boolean closed;
String world = "world";

@Override
public String call() throws Exception {
if (closed)
return "2goodbye, " + world;
else
return "2hello, " + world;
}

@Override
public void close() throws IOException {
System.out.println("closed");
closed = true;
}

@Override
public void setProperties(Map<String, String> map) throws Exception {
world = map.get("world");
}

@Override
public void setReporter(Reporter processor) {
// ignore
}

}
Expand Up @@ -11,11 +11,13 @@
import java.util.concurrent.Callable;

import org.junit.jupiter.api.Test;
import org.osgi.framework.VersionRange;

import aQute.bnd.build.Workspace;
import aQute.bnd.header.Attrs;
import aQute.bnd.repository.fileset.FileSetRepository;
import aQute.bnd.result.Result;
import aQute.bnd.service.externalplugin.ExternalPluginNamespace;
import aQute.bnd.service.generate.Generator;
import aQute.bnd.test.jupiter.InjectTemporaryDirectory;
import aQute.lib.io.FileTree;
Expand All @@ -40,7 +42,7 @@ public void testSimple() throws Exception {
});
System.out.println(call);
assertThat(call.isOk()).isTrue();
assertThat(call.unwrap()).isEqualTo("hello, world");
assertThat(call.unwrap()).isEqualTo("2hello, world");
}
}

Expand All @@ -56,10 +58,10 @@ public void testListOfImplementations() throws Exception {

assertThat(implementations.isOk()).isTrue();
List<Callable> unwrap = implementations.unwrap();
assertThat(unwrap).hasSize(1);
assertThat(unwrap).hasSize(2);

callable = unwrap.get(0);
assertThat(callable.call()).isEqualTo("hello, world");
assertThat(callable.call()).isEqualTo("2hello, world");
}
}

Expand All @@ -86,9 +88,9 @@ public void testAbstractPlugin() throws Exception {

plugin = ws.getPlugin(Callable.class);
assertThat(plugin).isNotNull();
assertThat(plugin.call()).isEqualTo("hello, plugin-attrs");
assertThat(plugin.call()).isEqualTo("2hello, plugin-attrs");
}
assertThat(plugin.call()).isEqualTo("goodbye, plugin-attrs");
assertThat(plugin.call()).isEqualTo("2goodbye, plugin-attrs");
}

@Test
Expand All @@ -103,6 +105,26 @@ public void testCallMainClass() throws Exception {
assertThat(call.isOk()).isTrue();
assertThat(new String(bout.toByteArray(), StandardCharsets.UTF_8)).contains("Hello world");
}
try (Workspace ws = getWorkspace("resources/ws-1")) {
getRepo(ws);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
Result<Integer> call = ws.getExternalPlugins()
.call("biz.aQute.bndall.tests.plugin_2.MainClass", new VersionRange("1.2.3"), ws,
Collections.emptyMap(),
Collections.emptyList(), null, bout, null);
System.out.println(call);
assertThat(call.isOk()).isTrue();
assertThat(new String(bout.toByteArray(), StandardCharsets.UTF_8)).contains("Hello world");
}
try (Workspace ws = getWorkspace("resources/ws-1")) {
getRepo(ws);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
Result<Integer> call = ws.getExternalPlugins()
.call("biz.aQute.bndall.tests.plugin_2.MainClass", new VersionRange("1.2.4"), ws,
Collections.emptyMap(), Collections.emptyList(), null, bout, null);
System.out.println(call);
assertThat(call.isOk()).isFalse();
}
}

@Test
Expand All @@ -119,6 +141,56 @@ public void testNotFound() throws Exception {
}
}

@SuppressWarnings("rawtypes")
@Test
public void testMultipleImplementationsWithVersion() throws Exception {
try (Workspace ws = getWorkspace("resources/ws-1")) {
getRepo(ws);
Attrs attrs = new Attrs();

Result<List<Callable>> implementations = ws.getExternalPlugins()
.getImplementations(Callable.class, attrs);

assertThat(implementations.isOk()).isTrue();
List<Callable> unwrap = implementations.unwrap();
assertThat(unwrap).hasSize(2);

assertThat(unwrap.get(0)
.call()).isEqualTo("2hello, world");
assertThat(unwrap.get(1)
.call()).isEqualTo("hello, world");
}
try (Workspace ws = getWorkspace("resources/ws-1")) {
getRepo(ws);
Attrs attrs = new Attrs();
attrs.put(ExternalPluginNamespace.VERSION_ATTRIBUTE, "[1.0.0,2.0.0)");

Result<List<Callable>> implementations = ws.getExternalPlugins()
.getImplementations(Callable.class, attrs);

assertThat(implementations.isOk()).isTrue();
List<Callable> unwrap = implementations.unwrap();
assertThat(unwrap).hasSize(1);
assertThat(unwrap.get(0)
.call()).isEqualTo("hello, world");

}
try (Workspace ws = getWorkspace("resources/ws-1")) {
getRepo(ws);
Attrs attrs = new Attrs();
attrs.put(ExternalPluginNamespace.VERSION_ATTRIBUTE, "2.0.0");

Result<List<Callable>> implementations = ws.getExternalPlugins()
.getImplementations(Callable.class, attrs);

assertThat(implementations.isOk()).isTrue();
List<Callable> unwrap = implementations.unwrap();
assertThat(unwrap).hasSize(1);
assertThat(unwrap.get(0)
.call()).isEqualTo("2hello, world");
}
}

private void getRepo(Workspace ws) throws IOException, Exception {
FileTree tree = new FileTree();
List<File> files = tree.getFiles(IO.getFile("generated"), "*.jar");
Expand Down
15 changes: 8 additions & 7 deletions biz.aQute.bndlib/src/aQute/bnd/build/ProjectGenerate.java
Expand Up @@ -212,18 +212,18 @@ private String doGenerate(String commandline, GeneratorSpec st) {
InputStream stdin = fstdin == null ? null : IO.stream(fstdin);
OutputStream stdout = fstdout == null ? null : IO.outputStream(fstdout);
OutputStream stderr = fstderr == null ? null : IO.outputStream(fstderr);
VersionRange range = st.version()
.map(VersionRange::valueOf)
.orElse(null);

try {
if (pluginName.indexOf('.') >= 0) {
if (pluginName.startsWith("."))
pluginName = pluginName.substring(1);

VersionRange range = st.version()
.map(VersionRange::valueOf)
.orElse(null);

result = doGenerateMain(pluginName, range, st._attrs(), arguments, stdin, stdout, stderr);
} else {
result = doGeneratePlugin(pluginName, st._attrs(), arguments, stdin, stdout, stderr);
result = doGeneratePlugin(pluginName, range, st._attrs(), arguments, stdin, stdout, stderr);
}
if (result != null)
return block + " : " + result;
Expand All @@ -245,13 +245,14 @@ private File getFile(String path, boolean mkdirs) {
return f;
}

private String doGeneratePlugin(String pluginName, Map<String, String> attrs, List<String> arguments,
private String doGeneratePlugin(String pluginName, VersionRange range, Map<String, String> attrs,
List<String> arguments,
InputStream stdin, OutputStream stdout, OutputStream stderr) {
BuildContext bc = new BuildContext(project, attrs, arguments, stdin, stdout, stderr);

Result<Boolean> call = project.getWorkspace()
.getExternalPlugins()
.call(pluginName, Generator.class, p -> {
.call(pluginName, range, Generator.class, p -> {

Class<?> type = SpecInterface.getParameterizedInterfaceType(p.getClass(), Generator.class);
if (type == null) {
Expand Down
Expand Up @@ -16,10 +16,10 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.osgi.framework.Version;
import org.osgi.framework.VersionRange;
import org.osgi.resource.Capability;
import org.slf4j.Logger;
Expand Down Expand Up @@ -55,18 +55,23 @@ public class WorkspaceExternalPluginHandler implements AutoCloseable {
}

public <T, R> Result<R> call(String pluginName, Class<T> c, FunctionWithException<T, Result<R>> f) {
return call(pluginName, null, c, f);
}

public <T, R> Result<R> call(String pluginName, VersionRange range, Class<T> c,
FunctionWithException<T, Result<R>> f) {
try {

String filter = ExternalPluginNamespace.filter(pluginName, c);

Optional<Capability> optCap = workspace
.findProviders(ExternalPluginNamespace.EXTERNAL_PLUGIN_NAMESPACE, filter)
.findAny();
List<Capability> caps = workspace.findProviders(ExternalPluginNamespace.EXTERNAL_PLUGIN_NAMESPACE, filter)
.sorted(this::sort)
.collect(Collectors.toList());

if (!optCap.isPresent())
if (caps.isEmpty())
return Result.err("no such plugin %s for type %s", pluginName, c.getName());

Capability cap = optCap.get();
Capability cap = caps.get(0);

Result<File> bundle = workspace.getBundle(cap.getResource());
if (bundle.isErr())
Expand Down Expand Up @@ -121,21 +126,20 @@ public Result<Integer> call(String mainClass, VersionRange range, Processor cont

String filter = MainClassNamespace.filter(mainClass, range);

Optional<Capability> optCap = workspace.findProviders(MainClassNamespace.MAINCLASS_NAMESPACE, filter)
.findAny();
List<Capability> caps = workspace.findProviders(MainClassNamespace.MAINCLASS_NAMESPACE, filter)
.sorted(this::sort)
.collect(Collectors.toList());

if (optCap.isPresent()) {
if (caps.isEmpty())
return Result.err("no bundle found with main class %s", mainClass);

Capability cap = optCap.get();
Capability cap = caps.get(0);

Result<File> bundle = workspace.getBundle(cap.getResource());
if (bundle.isErr())
return bundle.asError();
Result<File> bundle = workspace.getBundle(cap.getResource());
if (bundle.isErr())
return bundle.asError();

cp.add(bundle.unwrap());
} else if (cp.isEmpty()) {
return Result.err("no bundle found with main class %s", mainClass);
}
cp.add(bundle.unwrap());

Command c = new Command();

Expand Down Expand Up @@ -228,9 +232,16 @@ public <T> Result<List<T>> getImplementations(Class<T> interf, Attrs attrs) {
assert interf.isInterface();

try {
String filter = ExternalPluginNamespace.filter(attrs.getOrDefault("name", "*"), interf);
String v = attrs.getVersion();
VersionRange r = null;
if (v != null) {
r = VersionRange.valueOf(v);
}

String filter = ExternalPluginNamespace.filter(attrs.getOrDefault("name", "*"), interf, r);
List<Capability> externalCapabilities = workspace
.findProviders(ExternalPluginNamespace.EXTERNAL_PLUGIN_NAMESPACE, filter)
.sorted(this::sort)
.collect(Collectors.toList());

Class<?>[] interfaces = new Class[] {
Expand Down Expand Up @@ -382,4 +393,20 @@ public URL findResource(String name) {
return null;
}
}

private int sort(Capability a, Capability b) {
String av = a.getAttributes()
.getOrDefault("version", "0.0.0")
.toString();
String bv = b.getAttributes()
.getOrDefault("version", "0.0.0")
.toString();
try {
Version avv = new Version(av);
Version bvv = new Version(bv);
return bvv.compareTo(avv);
} catch (Exception e) {
return bv.compareTo(av);
}
}
}
2 changes: 1 addition & 1 deletion biz.aQute.bndlib/src/aQute/bnd/build/package-info.java
@@ -1,6 +1,6 @@
/**
*/
@Version("4.3.0")
@Version("4.4.0")
package aQute.bnd.build;

import org.osgi.annotation.versioning.Version;
Expand Up @@ -3,14 +3,31 @@
import org.osgi.annotation.bundle.Attribute;
import org.osgi.annotation.bundle.Capability;

/**
* Define a capability header for an External Plugin class.
*/
@Capability(namespace = ExternalPluginNamespace.EXTERNAL_PLUGIN_NAMESPACE, attribute = {
ExternalPluginNamespace.CAPABILITY_IMPLEMENTATION_ATTRIBUTE + "=${@class}"
})
public @interface ExternalPlugin {
/**
* The name of this plugin
*/
@Attribute(ExternalPluginNamespace.CAPABILITY_NAME_ATTRIBUTE)
String name();

/**
* The service/plugin type of the plugin
*/
@Attribute(ExternalPluginNamespace.CAPABILITY_OBJECTCLASS_ATTRIBUTE)
Class<?> objectClass();

/**
* The version
*
* @return the version
*/
@Attribute(ExternalPluginNamespace.VERSION_ATTRIBUTE + ":Version")
String version() default "0.0.0";

}

0 comments on commit 8762af5

Please sign in to comment.