Skip to content

Commit 268d59e

Browse files
committed
Update hte readme.
1 parent 4578204 commit 268d59e

1 file changed

Lines changed: 94 additions & 98 deletions

File tree

README.md

Lines changed: 94 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -26,145 +26,141 @@ output = [
2626
## AtPlug is...
2727

2828
- a plugin system for the JVM
29-
- that generates all OSGi metadata for you - write Java code, not error-prone metadata
30-
- that runs with or without OSGi
31-
+ No need for OSGi in small systems (e.g. unit tests)
32-
+ Take full advantage of OSGi's power in large systems
29+
- written in pure Kotlin, might port to Kotlin Multiplatform [someday](https://github.com/diffplug/atplug/issues/1).
30+
- that generates all plugin metadata for you
31+
- write Java/Kotlin/Scala code, *never* write error-prone metadata
32+
- lets you filter the available plugins based on their metadata
33+
- defer classloading to the last possible instant
34+
- easy mocking for unit tests
3335

34-
AtPlug has two components:
36+
AtPlug has three components:
3537

36-
- a small runtime (less than 1000 lines) which allows seamless operation inside and outside of OSGi
37-
- a buildtime step which generates OSGi declarative service metadata
38-
+ Gradle plugin: [`com.diffplug.gradle.atplug`](https://plugins.gradle.org/plugin/com.diffplug.gradle.atplug)
39-
+ Contributions welcome for maven, ant, etc.
38+
- a small runtime `com.diffplug.atplug:atplug-runtime`
39+
- a buildtime step which generates plugin metadata
40+
+ Gradle plugin: [`com.diffplug.atplug`](https://plugins.gradle.org/plugin/com.diffplug.atplug)
41+
+ Contributions welcome for maven, etc.
42+
- a small harness for unit test mocking `com.diffplug.atplug:atplug-test-harness`
4043

41-
It is currently in production usage at [DiffPlug](https://www.diffplug.com); extracting it into an opensource project is a WIP.
44+
It is in production usage at [DiffPlug](https://www.diffplug.com).
4245

4346
## How it works
4447

45-
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:
48+
Let's say you're building a drawing application, and you want a plugin system to allow users to contribute different shapes. The socket interface might look something like this:
4649

47-
```java
48-
public interface FileMenu {
49-
/** Adds the appropriate entries for the given list of files. */
50-
void addRightClick(Menu root, List<File> files);
50+
```kotlin
51+
interface Shape {
52+
fun draw(g: Graphics)
5153
}
5254
```
5355

54-
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.
56+
Let's say our system has 100 different `Shape` plugins. Loading all 100 plugins will take a long time, so we'd like to describe which shapes are available without having to actually load it.
5557

5658
We can accomplish this in AtPlug 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.
5759

58-
```java
59-
public interface FileMenu {
60-
/** Extensions for which this FileMenu applies (empty set means it applies to all extensions). */
61-
@Metadata default Set<String> extensions() {
62-
return Collections.emptySet();
63-
}
64-
65-
/** Adds the appropriate entries for the given list of files. */
66-
void addRightClick(Menu root, List<File> files);
60+
```kotlin
61+
interface Shape {
62+
@Metadata fun name(): String
63+
@Metadata fun previewSvgIcon(): String
64+
fun draw(g: Graphics)
6765
}
6866
```
6967

70-
The OSGi runtime (and AtPlug'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.
71-
72-
To take advantage of this, we need to declare a class `FileMenu.MetadataCreator extends `[`DeclarativeMetadataCreator<FileMenu>`](TODO-javadoc), 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.
68+
The AtPlug runtime stores 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 `Shape` plugins in the system without loading their classes.
7369

74-
In order to read this metadata at runtime, we also need to declare a class `FileMenu.Descriptor extends `[`ServiceDescriptor<FileMenu>`](TODO-javadoc) which will parse the `Map<String, String>` into a convenient form for determining which plugins to load.
70+
To take advantage of this, we need to an object `Shape.Socket : SocketOwner` which will take a `Shape` instance and return a `Map<String, String>`. This will be used during the build step to generate AtPlug metadata files.
7571

76-
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.
77-
78-
Here's how we might implement our FileMenu.MetadataCreator and FileMenu.Descriptor.
79-
80-
```java
81-
public interface FileMenu {
82-
...
83-
/** Generates metadata from an instance of FileMenu (implementation detail). */
84-
static class MetadataCreator extends DeclarativeMetadataCreator<FileMenu> {
85-
private static final String KEY_EXTENSIONS = "extensions";
86-
87-
public MetadataCreator() {
88-
super(FileMenu.class, instance -> ImmutableMap.of(KEY_EXTENSIONS, Converters.forSet().convert(instance.fsPrefixes()));
89-
}
72+
```kotlin
73+
interface Shape {
74+
object Socket : SocketOwner.SingletonById<Shape>(Shape::class.java) {
75+
const val KEY_SVG_ICON = "svgIcon"
76+
override fun metadata(plug: Fruit) = mapOf(
77+
Pair(KEY_ID, plug.name()),
78+
Pair(KEY_SVG_ICON, plug.previewSvgIcon()))
9079
}
80+
}
81+
```
9182

92-
/**
93-
* Parses a descriptor of a FileMenu from its metadata.
94-
* Public API for exploring the registry of available plugins.
95-
*/
96-
public static final class Descriptor extends ServiceDescriptor<FileMenu> {
97-
final Set<String> extensions;
83+
Now your users can declare an instance of `Shape` and annotate it with `@Plug(Shape.class)`.
9884

99-
private Descriptor(ServiceReference<FileMenu> ref) {
100-
super(ref);
101-
this.extensions = Converters.forSet().reverse().convert(getString(MetadataCreator.KEY_EXTENSIONS));
102-
}
85+
```kotlin
86+
@Plug(Shape::class)
87+
class Circle : Shape {
88+
override fun name() = "Circle"
89+
override fun previewSvgIcon() = "icons/circle.svg"
90+
override fun draw(g: Graphics) = g.drawCircle()
91+
}
92+
```
10393

104-
private boolean appliesTo(List<Filder> files) {
105-
return extensions.stream().allMatch(extension -> {
106-
return files.stream().allMatch(file -> file.getName().endsWith(extension));
107-
});
108-
}
94+
Now when you run `./gradlew jar`, you will have a resource file called `ATPLUG-INF/com.package.Circle.json` with content like this:
10995

110-
/** Returns descriptors for all RightClickFiles which apply to the given list of files. */
111-
public static Stream<Descriptor> getFor(List<Filder> files) {
112-
return ServiceDescriptor.getServices(FileMenu.class, Descriptor::new).filter(d -> d.appliesTo(files));
113-
}
96+
```json
97+
{ "implementation": "com.package.Circle",
98+
"provides": "com.api.Shape",
99+
"properties": {
100+
"id": "Circle",
101+
"svgIcon": "icons/circle.svg"
114102
}
115103
}
116104
```
117105

118-
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.
106+
And the manifest of the Jar file will have a field `AtPlug-Component` which points to all the json files in the `ATPLUG-INF` directory. You never have to edit these files, but there's no magic. The `metadata` function which you wrote for the socket generates all the json files.
119107

120-
```java
121-
@Plug
122-
public class DocxFileMenu implements FileMenu {
123-
/** Extensions for which this FileMenu applies (empty set means it applies to all extensions). */
124-
@Override public Set<String> extensions() {
125-
return ImmutableSet.of("doc", "docx");
126-
}
108+
To use the plugin system, you can do:
127109

128-
/** Adds the appropriate entries for the given list of files. */
129-
@Override public void addRightClick(Menu root, List<File> files) {
130-
// do stuff
131-
}
132-
}
110+
```kotlin
111+
Shape.Socket.allIds(): Set<String>
112+
Shape.Socket.descriptorForId(id: String): PlugDescriptor?
113+
Shape.Socket.forId(id: String): Shape?
133114
```
134115

135-
When we run `gradlew generateOsgiMetadata` (which will run automatically whenever it is needed), AtPlug's build step will generate these files for us:
116+
Which are all built-in to every sublass of `SocketOwner.SingletonById`. You can add more methods too for your usecase.
136117

137-
```
138-
--- OSGI-INF/com.diffplug.talks.socketsandplugs.DocxFileMenu.xml ---
139-
<component name="com.diffplug.talks.socketsandplugs.DocxFileMenu">
140-
<implementation class="com.diffplug.talks.socketsandplugs.DocxFileMenu"></implementation>
141-
<service>
142-
<provide interface="com.diffplug.talks.socketsandplugs.FileMenu"></provide>
143-
</service>
144-
<property name="extensions" type="String" value="doc,docx"></property>
145-
</component>
146-
147-
--- META-INF/MANIFEST.MF ---
148-
Service-Component: OSGI-INF/com.diffplug.talks.socketsandplugs.DocxFileMenu.xml
149-
```
118+
### (Id vs Descriptor) and (Singleton vs Ephemeral)
119+
120+
The `Socket` is responsible for:
121+
122+
- generating metadata (at buildtime)
123+
- maintaining the runtime registry of available plugins
124+
- instantiating the actual objects from their metadata
125+
126+
When it comes to the registry of available plugins, there are two obvious design points
150127

151-
AtPlug 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). AtPlug just does all the plumbing and grunt work for you.
128+
- declare some String which functions as a unique id => `Id`
129+
- parse the `Map<String, String>` into a descriptor class, and run filters against the set of parsed descriptors to get all the plugins which apply to the given situation => `Descriptor`.
152130

153-
To use the plugin system, all you have to do is:
131+
When it comes to instantiating the actual objects from their metadata, there are again two obvious designs:
132+
133+
- Once a plugin is instantiated, cache it forever and return the same instance each time => `Singleton`
134+
- Call the plugin constructor each time it is instantiated, so that you may end up with multiple instances of a single plugin => `Ephemeral`
135+
136+
In most cases, if a plugin has a unique id, then it also makes sense to treat that plugin as a global singleton => `SocketOwner.SingletonById`. Likewise, if plugins do not have unique ids, then their concept of identity probably doesn't matter so there's no need to cache them as singletons => `SocketOwner.EphemeralByDescriptor`.
137+
138+
Those two classes, `SingletonById` and `EphemeralByDescriptor`, are the only two options we provide out of the box - we did not fill the full 2x2 matrix. For every case we have encountered, we can easily extend one or the other and get exactly what we need.
139+
140+
But you are free to implement `SocketOwner` yourself from scratch if you want a different design point.
141+
142+
### Working from Java
143+
144+
The examples above are Kotlin, but you can also use Java. To declare the socket, just have a field `static final SocketOwner socket`, as shown below:
154145

155146
```java
156-
Menu root = new Menu();
157-
List<File> files = Arrays.asList(new File("Budget.docx"));
158-
for (FileMenu.Descriptor descriptor : FileMenu.Descriptor.getFor(files)) {
159-
descriptor.openManaged(instance -> {
160-
instance.addRightClick(root, files);
161-
});
147+
public interface Shape {
148+
public static final SocketOwner.Id<Shape> socket = new SocketOwner.SingletonId<Shape>(Shape.class) {
149+
@Override
150+
public Map<String, String> metadata(Shape plug) {
151+
Map<String, String> map = new HashMap<>();
152+
map.put(KEY_ID, plug.name());
153+
return map;
154+
}
155+
};
162156
}
163157
```
164158

159+
Sockets don't have to be interfaces - abstract classes or even concrete classes would work fine too.
160+
165161
## Requirements
166162

167-
Nothing so far...
163+
Java 8+.
168164

169165
## Acknowledgements
170166

0 commit comments

Comments
 (0)