You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- a small harness for unit test mocking `com.diffplug.atplug:atplug-test-harness`
40
43
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).
42
45
43
46
## How it works
44
47
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:
46
49
47
-
```java
48
-
publicinterfaceFileMenu {
49
-
/** Adds the appropriate entries for the given list of files. */
50
-
voidaddRightClick(Menuroot, List<File>files);
50
+
```kotlin
51
+
interfaceShape {
52
+
fundraw(g:Graphics)
51
53
}
52
54
```
53
55
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.
55
57
56
58
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.
57
59
58
-
```java
59
-
publicinterfaceFileMenu {
60
-
/** Extensions for which this FileMenu applies (empty set means it applies to all extensions). */
61
-
@Metadata default Set<String>extensions() {
62
-
returnCollections.emptySet();
63
-
}
64
-
65
-
/** Adds the appropriate entries for the given list of files. */
66
-
voidaddRightClick(Menuroot, List<File>files);
60
+
```kotlin
61
+
interfaceShape {
62
+
@Metadata funname(): String
63
+
@Metadata funpreviewSvgIcon(): String
64
+
fundraw(g:Graphics)
67
65
}
68
66
```
69
67
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.
73
69
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.
75
71
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
-
publicinterfaceFileMenu {
82
-
...
83
-
/** Generates metadata from an instance of FileMenu (implementation detail). */
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.
119
107
120
-
```java
121
-
@Plug
122
-
publicclassDocxFileMenuimplementsFileMenu {
123
-
/** Extensions for which this FileMenu applies (empty set means it applies to all extensions). */
124
-
@OverridepublicSet<String>extensions() {
125
-
returnImmutableSet.of("doc", "docx");
126
-
}
108
+
To use the plugin system, you can do:
127
109
128
-
/** Adds the appropriate entries for the given list of files. */
### (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
150
127
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`.
152
130
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:
0 commit comments