Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[external plugins] Added versioning support #5388

Merged
merged 1 commit into from Oct 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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";

}