AutOSGi: Sockets and Plugs without the boilerplate
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
.ci
_images
gradle
lib-runtime
plugin-gradle
.gitignore
.travis.yml
CHANGES.md
CONTRIBUTING.md
LICENSE
README.md
build.gradle
build.properties
gradle.properties
gradlew
gradlew.bat
settings.gradle

README.md

AutOSGi: Sockets and Plugs without boilerplate

Maven artifact Gradle plugin

Latest version Javadoc License Apache Changelog Live chat

AutOSGi is...

  • a plugin system for the JVM
  • that generates all OSGi metadata for you - write Java code, not error-prone metadata
  • that runs with or without OSGi
    • No need for OSGi in small systems (e.g. unit tests)
    • Take full advantage of OSGi's power in large systems

AutOSGi has two components:

  • a small runtime (less than 1000 lines) which allows seamless operation inside and outside of OSGi
  • a buildtime step which generates OSGi declarative service metadata

It is currently in production usage at DiffPlug; extracting it into an opensource project is a WIP.

How it works

Let's say you're building a filesystem explorer, and you'd like to present a plugin interface for adding items to the right click menu. The socket interface might look something like this:

public interface FileMenu {
	/** Adds the appropriate entries for the given list of files. */
	void addRightClick(Menu root, List<File> files);
}

Let's say our system has 100 different FileMenu plugins. Loading all 100 plugins will take a long time, so we'd like to describe which files a given FileMenu applies to without having to actually load it. One way would be if each FileMenu declared which file extensions it is applicable to.

We can accomplish this in AutOSGi by adding a method to the socket interface marked with @Metadata. The annotation is a documentation hint that this method should return a constant value which will be used to generate static metadata about the plugin.

public interface FileMenu {
	/** Extensions for which this FileMenu applies (empty set means it applies to all extensions). */
	@Metadata default Set<String> extensions() {
		return Collections.emptySet();
	}

	/** Adds the appropriate entries for the given list of files. */
	void addRightClick(Menu root, List<File> files);
}

The OSGi runtime (and AutOSGi's non-OSGi compatibility layer) can store metadata about a plugin in a Map<String, String> which gets saved into a metadata file. This is the mechanism which allows us to inspect all the FileMenu plugins in the system without loading their classes.

To take advantage of this, we need to declare a class FileMenu.MetadataCreator extends DeclarativeMetadataCreator<FileMenu>, which will take a FileMenu instance and return a Map<String, String> (a.k.a. Function<FileMenu, Map<String, String>>). This will be used during the build step to generate OSGi metadata files.

In order to read this metadata at runtime, we also need to declare a class FileMenu.Descriptor extends ServiceDescriptor<FileMenu> which will parse the Map<String, String> into a convenient form for determining which plugins to load.

In the case of our FileMenu socket, implementing MetadataCreator and Descriptor mostly boils down to turning a Set<String> of extensions into a Map<String, String>. There are lots of ways to do this, but the clearest is probably to turn the set [doc, docx] into the map extensions=doc,docx, where we encode the set using a single comma-delimited string. This way if we decide later to add other metadata like int minFiles() or int maxFiles(), then we can trivially update the metadata map to extensions=doc,docx minFiles=1 maxFiles=1. The project [durian-parse](TODO-link) has a variety of useful converters for going back and forth between simple data structures and raw strings.

Here's how we might implement our FileMenu.MetadataCreator and FileMenu.Descriptor.

public interface FileMenu {
	...
	/** Generates metadata from an instance of FileMenu (implementation detail). */
	static class MetadataCreator extends DeclarativeMetadataCreator<FileMenu> {
		private static final String KEY_EXTENSIONS = "extensions";

		public MetadataCreator() {
			super(FileMenu.class, instance -> ImmutableMap.of(KEY_EXTENSIONS, Converters.forSet().convert(instance.fsPrefixes()));
		}
	}

	/**
	* Parses a descriptor of a FileMenu from its metadata.
	* Public API for exploring the registry of available plugins.
	*/
	public static final class Descriptor extends ServiceDescriptor<FileMenu> {
		final Set<String> extensions;

		private Descriptor(ServiceReference<FileMenu> ref) {
			super(ref);
			this.extensions = Converters.forSet().reverse().convert(getString(MetadataCreator.KEY_EXTENSIONS));
		}

		private boolean appliesTo(List<Filder> files) {
			return extensions.stream().allMatch(extension -> {
				return files.stream().allMatch(file -> file.getName().endsWith(extension));
			});
		}

		/** Returns descriptors for all RightClickFiles which apply to the given list of files. */
		public static Stream<Descriptor> getFor(List<Filder> files) {
			return ServiceDescriptor.getServices(FileMenu.class, Descriptor::new).filter(d -> d.appliesTo(files));
		}
	}
}

Now, when we want to implement a right-click menu, all we have to do is mark it with the @Plug annotation so that the build step can find it.

@Plug
public class DocxFileMenu implements FileMenu {
	/** Extensions for which this FileMenu applies (empty set means it applies to all extensions). */
	@Override public Set<String> extensions() {
		return ImmutableSet.of("doc", "docx");
	}

	/** Adds the appropriate entries for the given list of files. */
	@Override public void addRightClick(Menu root, List<File> files) {
		// do stuff
	}
}

When we run gradlew generateOsgiMetadata (which will run automatically whenever it is needed), AutOSGi's build step will generate these files for us:

--- OSGI-INF/com.diffplug.talks.socketsandplugs.DocxFileMenu.xml ---
<component name="com.diffplug.talks.socketsandplugs.DocxFileMenu">
	<implementation class="com.diffplug.talks.socketsandplugs.DocxFileMenu"></implementation>
	<service>
		<provide interface="com.diffplug.talks.socketsandplugs.FileMenu"></provide>
	</service>
	<property name="extensions" type="String" value="doc,docx"></property>
</component>

--- META-INF/MANIFEST.MF ---
Service-Component: OSGI-INF/com.diffplug.talks.socketsandplugs.DocxFileMenu.xml

AutOSGi ensures that you'll never have to edit these files by hand, but there's no magic. You write the function that generates the metadata (MetadataCreator) and you write the function that parses the metadata (Descriptor). AutOSGi just does all the plumbing and grunt work for you.

To use the plugin system, all you have to do is:

Menu root = new Menu();
List<File> files = Arrays.asList(new File("Budget.docx"));
for (FileMenu.Descriptor descriptor : FileMenu.Descriptor.getFor(files)) {
	descriptor.openManaged(instance -> {
		instance.addRightClick(root, files);
	});
}

Requirements

Nothing so far...

Acknowledgements