From ed4db272128e27ac8e0f1c172937f32cac40b55b Mon Sep 17 00:00:00 2001 From: Peter Kriens Date: Fri, 30 Sep 2022 11:44:55 +0200 Subject: [PATCH] [external plugins] Added versioning support - Will not pick highest version when multiple plugins are found - Version range attribute supported on external plugins - Added version range to @ExternalPlugin Signed-off-by: Peter Kriens --- biz.aQute.bndall.tests/plugin-2.bnd | 2 +- .../bndall/tests/plugin_1/CallablePlugin.java | 2 +- .../tests/plugin_1/CallablePlugin2.java | 41 ++++++++++ .../ExternalPluginHandlerTest.java | 82 +++++++++++++++++-- .../src/aQute/bnd/build/ProjectGenerate.java | 15 ++-- .../build/WorkspaceExternalPluginHandler.java | 63 ++++++++++---- .../src/aQute/bnd/build/package-info.java | 2 +- .../externalplugin/ExternalPlugin.java | 17 ++++ .../ExternalPluginNamespace.java | 16 ++++ .../service/externalplugin/package-info.java | 2 +- docs/_chapters/875-external-plugins.md | 1 + docs/_instructions/generate.md | 5 +- 12 files changed, 213 insertions(+), 35 deletions(-) create mode 100644 biz.aQute.bndall.tests/src/biz/aQute/bndall/tests/plugin_1/CallablePlugin2.java diff --git a/biz.aQute.bndall.tests/plugin-2.bnd b/biz.aQute.bndall.tests/plugin-2.bnd index 6e07fbb0bc..16673d0638 100644 --- a/biz.aQute.bndall.tests/plugin-2.bnd +++ b/biz.aQute.bndall.tests/plugin-2.bnd @@ -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 \ No newline at end of file +Bundle-Version: 1.2.3 \ No newline at end of file diff --git a/biz.aQute.bndall.tests/src/biz/aQute/bndall/tests/plugin_1/CallablePlugin.java b/biz.aQute.bndall.tests/src/biz/aQute/bndall/tests/plugin_1/CallablePlugin.java index f201efb4fc..bf7e06a9f9 100644 --- a/biz.aQute.bndall.tests/src/biz/aQute/bndall/tests/plugin_1/CallablePlugin.java +++ b/biz.aQute.bndall.tests/src/biz/aQute/bndall/tests/plugin_1/CallablePlugin.java @@ -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, Closeable, Plugin { boolean closed; String world = "world"; diff --git a/biz.aQute.bndall.tests/src/biz/aQute/bndall/tests/plugin_1/CallablePlugin2.java b/biz.aQute.bndall.tests/src/biz/aQute/bndall/tests/plugin_1/CallablePlugin2.java new file mode 100644 index 0000000000..24f83c26fb --- /dev/null +++ b/biz.aQute.bndall.tests/src/biz/aQute/bndall/tests/plugin_1/CallablePlugin2.java @@ -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, 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 map) throws Exception { + world = map.get("world"); + } + + @Override + public void setReporter(Reporter processor) { + // ignore + } + +} diff --git a/biz.aQute.bndall.tests/test/biz/aQute/externalplugin/ExternalPluginHandlerTest.java b/biz.aQute.bndall.tests/test/biz/aQute/externalplugin/ExternalPluginHandlerTest.java index 271ba6417f..81c26e8d97 100644 --- a/biz.aQute.bndall.tests/test/biz/aQute/externalplugin/ExternalPluginHandlerTest.java +++ b/biz.aQute.bndall.tests/test/biz/aQute/externalplugin/ExternalPluginHandlerTest.java @@ -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; @@ -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"); } } @@ -56,10 +58,10 @@ public void testListOfImplementations() throws Exception { assertThat(implementations.isOk()).isTrue(); List 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"); } } @@ -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 @@ -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 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 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 @@ -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> implementations = ws.getExternalPlugins() + .getImplementations(Callable.class, attrs); + + assertThat(implementations.isOk()).isTrue(); + List 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> implementations = ws.getExternalPlugins() + .getImplementations(Callable.class, attrs); + + assertThat(implementations.isOk()).isTrue(); + List 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> implementations = ws.getExternalPlugins() + .getImplementations(Callable.class, attrs); + + assertThat(implementations.isOk()).isTrue(); + List 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 files = tree.getFiles(IO.getFile("generated"), "*.jar"); diff --git a/biz.aQute.bndlib/src/aQute/bnd/build/ProjectGenerate.java b/biz.aQute.bndlib/src/aQute/bnd/build/ProjectGenerate.java index d3f0741a9b..7748e8de1c 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/build/ProjectGenerate.java +++ b/biz.aQute.bndlib/src/aQute/bnd/build/ProjectGenerate.java @@ -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; @@ -245,13 +245,14 @@ private File getFile(String path, boolean mkdirs) { return f; } - private String doGeneratePlugin(String pluginName, Map attrs, List arguments, + private String doGeneratePlugin(String pluginName, VersionRange range, Map attrs, + List arguments, InputStream stdin, OutputStream stdout, OutputStream stderr) { BuildContext bc = new BuildContext(project, attrs, arguments, stdin, stdout, stderr); Result 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) { diff --git a/biz.aQute.bndlib/src/aQute/bnd/build/WorkspaceExternalPluginHandler.java b/biz.aQute.bndlib/src/aQute/bnd/build/WorkspaceExternalPluginHandler.java index 8c30288728..ea9c0592b5 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/build/WorkspaceExternalPluginHandler.java +++ b/biz.aQute.bndlib/src/aQute/bnd/build/WorkspaceExternalPluginHandler.java @@ -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; @@ -55,18 +55,23 @@ public class WorkspaceExternalPluginHandler implements AutoCloseable { } public Result call(String pluginName, Class c, FunctionWithException> f) { + return call(pluginName, null, c, f); + } + + public Result call(String pluginName, VersionRange range, Class c, + FunctionWithException> f) { try { String filter = ExternalPluginNamespace.filter(pluginName, c); - Optional optCap = workspace - .findProviders(ExternalPluginNamespace.EXTERNAL_PLUGIN_NAMESPACE, filter) - .findAny(); + List 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 bundle = workspace.getBundle(cap.getResource()); if (bundle.isErr()) @@ -121,21 +126,20 @@ public Result call(String mainClass, VersionRange range, Processor cont String filter = MainClassNamespace.filter(mainClass, range); - Optional optCap = workspace.findProviders(MainClassNamespace.MAINCLASS_NAMESPACE, filter) - .findAny(); + List 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 bundle = workspace.getBundle(cap.getResource()); - if (bundle.isErr()) - return bundle.asError(); + Result 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(); @@ -228,9 +232,16 @@ public Result> getImplementations(Class 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 externalCapabilities = workspace .findProviders(ExternalPluginNamespace.EXTERNAL_PLUGIN_NAMESPACE, filter) + .sorted(this::sort) .collect(Collectors.toList()); Class[] interfaces = new Class[] { @@ -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); + } + } } diff --git a/biz.aQute.bndlib/src/aQute/bnd/build/package-info.java b/biz.aQute.bndlib/src/aQute/bnd/build/package-info.java index 370d8caa13..c2000bf5d5 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/build/package-info.java +++ b/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; diff --git a/biz.aQute.bndlib/src/aQute/bnd/service/externalplugin/ExternalPlugin.java b/biz.aQute.bndlib/src/aQute/bnd/service/externalplugin/ExternalPlugin.java index d254e6b316..e2b21be6ee 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/service/externalplugin/ExternalPlugin.java +++ b/biz.aQute.bndlib/src/aQute/bnd/service/externalplugin/ExternalPlugin.java @@ -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"; + } diff --git a/biz.aQute.bndlib/src/aQute/bnd/service/externalplugin/ExternalPluginNamespace.java b/biz.aQute.bndlib/src/aQute/bnd/service/externalplugin/ExternalPluginNamespace.java index 1ebebf92d5..572769b26d 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/service/externalplugin/ExternalPluginNamespace.java +++ b/biz.aQute.bndlib/src/aQute/bnd/service/externalplugin/ExternalPluginNamespace.java @@ -1,6 +1,7 @@ package aQute.bnd.service.externalplugin; import org.osgi.framework.Constants; +import org.osgi.framework.VersionRange; import org.osgi.resource.Capability; /** @@ -39,7 +40,16 @@ public interface ExternalPluginNamespace { */ String CAPABILITY_IMPLEMENTATION_ATTRIBUTE = "implementation"; + /** + * The version of this bundle as set by Bundle-Version, not set if absent + */ + String VERSION_ATTRIBUTE = "version"; + static String filter(String name, Class type) { + return filter(name, type, null); + } + + static String filter(String name, Class type, VersionRange range) { StringBuilder sb = new StringBuilder(); sb.append("(&(") .append(NAME_A) @@ -52,6 +62,12 @@ static String filter(String name, Class type) { .append(")(") .append(IMPLEMENTATION_A) .append("=*))"); + if (range != null) { + sb.insert(0, "(&") + .append(range.toFilterString(VERSION_ATTRIBUTE)) + .append(')'); + } + return sb.toString(); } diff --git a/biz.aQute.bndlib/src/aQute/bnd/service/externalplugin/package-info.java b/biz.aQute.bndlib/src/aQute/bnd/service/externalplugin/package-info.java index 16f54be454..cada30aa96 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/service/externalplugin/package-info.java +++ b/biz.aQute.bndlib/src/aQute/bnd/service/externalplugin/package-info.java @@ -1,2 +1,2 @@ -@org.osgi.annotation.versioning.Version("1.1.0") +@org.osgi.annotation.versioning.Version("1.2.0") package aQute.bnd.service.externalplugin; diff --git a/docs/_chapters/875-external-plugins.md b/docs/_chapters/875-external-plugins.md index 221458bfd8..d1d443f859 100644 --- a/docs/_chapters/875-external-plugins.md +++ b/docs/_chapters/875-external-plugins.md @@ -13,6 +13,7 @@ External Plugins are external code to bnd code but that can be executed from wit | objectClass | The interface type the implementation should implement| | implementation | The implementation fully qualified class name| | subtype | Optional subtype when the `objectClass` has a type parameter | +| version | Optional version range to limit the candidates | There is an annotation, `aQute.bnd.service.externalplugin.ExternalPlugin`, that can be applied on a type. diff --git a/docs/_instructions/generate.md b/docs/_instructions/generate.md index 1ae512b7d3..27fb99a86a 100644 --- a/docs/_instructions/generate.md +++ b/docs/_instructions/generate.md @@ -18,6 +18,7 @@ This `-generate` instruction specifies the code generating steps that must be ex | 'classpath=' PATH | 'workingdir=' FILE | 'clear=' BOOLEAN + | 'version=' RANGE For each clause, the key of the clause is used to establish an Ant File Set, e.g. `foo/**.in`. This a glob expression with the exception that the double wildcard ('**') indicates to any depth of directories. The `output` attribute _must_ specify a directory. If the output must be compiled this directory must be on the bnd source path. @@ -38,7 +39,9 @@ Without a dot in the name, the name is assumes to be an _external plugin_ name, The `generate` value is a _command line_. It can use the standard _unix_ like way of specifying a command. It supports flags (boolean parameters) and parameter that take a value. When this external plugin is executed, it is expected to create files fall within the _target_, if not, an error is reported. These changed or created files are refreshed in Eclipse. -The command line can be broken in different commands with the semicolon (`';'`), like a unix shell. Redirection of stdin (`'<'`), stdout (`'>'`, or `'1>'`), and stderr (`'2>'`) are supported. The path for redirection is relative to the project directory, even if `workingdir` has been specified. +The command line can be broken in different commands with the semicolon (`';'`), like a unix shell. Redirection of stdin (`'<'`), stdout (`'>'`, or `'1>'`), and stderr (`'2>'`) are supported. The path for redirection is relative to the project directory, even if `workingdir` has been specified. + +With the `version` version range attribute it is possible to restrict the candidates if there are multiple versions available. The code will select the highest version if only one is used. ## Example with an External Plugin