Plugin Development

Plugins can extend the REST and GraphQL API and also hook into various places of Gentics Mesh. A plugin developer needs to extend the AbstractPlugin class. The plugin can hook into different parts of Gentics Mesh by implementing one of multiple interfaces for the different plugin types. The plugin code needs to be packages as a regular jar file.

In general the plugin developer just needs to implement the methods which will be invoked by Gentics Mesh.

Plugin Setup

New Plugin classes need to extend the AbstractPlugin and add the mandatory constructor.

public class HelloWorldPlugin extends AbstractPlugin {

	public BasicPlugin(PluginWrapper wrapper, PluginEnvironment env) {
		super(wrapper, env);
	}

}

Plugin Methods

Additionally the plugin class provides some utility functions which make it easier to handle plugin configuration and plugin data.

The configuration of the plugin will be stored in the plugin directory in a plugins/{pluginId}/config.yml file which can be overridden by a /plugins/{pluginId}/config.local.yml file.

Method Description

AbstractPlugin#writeConfig(config)

Write the configuration POJO to the config.yml file.

AbstractPlugin#readConfig(clazz)

Read the configuration using the class. This method will also use the config.local.yml values if present.

AbstractPlugin#getConfigFile()

Return the config file.

AbstractPlugin#getLocalConfigFile()

Return the local config file.

AbstractPlugin#getStorageDir()

Return the path to the storage directory plugins/{pluginId}/storage in which a plugin can place files in the filesystem.

Plugin Manifest

The manifest of a plugin contains metadata information which will is used to load and categorize the plugin.

Table 1. Plugin Manifest
Name Description

Plugin-Id

Unique id of the plugin. Used to access the plugin once deployed.

Plugin-Name

Full name of the plugin

Plugin-Version

Current version of the plugin

Plugin-Author

Author of the plugin

Plugin-Class

Fully qualified path to the plugin class

Plugin-Description

Short description of the plugin

Plugin-License

License used by the plugin

Plugin-Inception

Date when the plugin was initially created (DD-MM-YYYY)

Plugin-Dependencies

List of plugin ids of other plugins on which the plugin depends.

Maven

The plugin manifest can directly be added when generating the shaded jar file.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <manifestEntries>
                            <Plugin-Id>${plugin.id}</Plugin-Id>
                            <Plugin-Name>${plugin.name}</Plugin-Name>
                            <Plugin-Version>${plugin.version}</Plugin-Version>
                            <Plugin-Author>${plugin.author}</Plugin-Author>
                            <Plugin-Class>${plugin.class}</Plugin-Class>
                            <Plugin-Description>${plugin.description}</Plugin-Description>
                            <Plugin-License>${plugin.license}</Plugin-License>
                            <Plugin-Inception>${plugin.inception}</Plugin-Inception>
                        </manifestEntries>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

Dependencies

All the needed maven artifact are located at our Maven Repository. Please add the following section to your pom.xml in order to be able to download the dependencies.

<repositories>
    <repository>
        <id>maven.gentics.com</id>
        <name>Gentics Maven Repository</name>
        <url>https://maven.gentics.com/maven2</url>
        <releases>
            <enabled>true</enabled>
        </releases>
    </repository>
</repositories>

The dependency version management can be managed more efficiently by just including the mesh-plugin-bom dependency. This will import the dependencyManagement for all plugin related dependencies.

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.gentics.mesh</groupId>
            <artifactId>mesh-plugin-bom</artifactId>
            <version>${mesh.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

The plugin needs at least the mesh-plugin-dep dependency. It is important to set the scope to provided. Otherwise the plugin can’t be loaded by Gentics Mesh.

<dependency>
    <groupId>com.gentics.mesh</groupId>
    <artifactId>mesh-plugin-dep</artifactId>
    <scope>provided</scope>
</dependency>

Access Data

Plugins can access Gentics Mesh data via the provided clients.

The adminClient will use the admin permissions and can by-default read everything. The userClient on the other hand will be created using the user information from the request that has been send to the plugin.

router.route("/example").handler(rc -> {
    PluginContext context = wrap(rc);
    MeshRestClient adminClient = adminClient();
    MeshRestClient userClient = context.client();
    …
});

Plugin Extensions

Plugins can also use and provide extensions. Extension can be used to modularize plugins. A plugin can for example provide a default implementation of a feature and another plugin can provide an extension which overrides or extends this feature.

The plugin (consumer) which uses the extension must provide an ExtensionPoint interface. This interface forms the contract which other plugins (provider) may use to implement the extension.

public interface DummyExtensionPoint extends ExtensionPoint {

	String name();

}

The plugin which provides the extension now implements the extension.

@Extension
public class DummyExtension implements DummyExtensionPoint {

	@Override
	public String name() {
		return "My dummy extension";
	}

}

Finally the consuming plugin may access the available extension via the plugin manager.

getWrapper().getPluginManager().getExtensions(DummyExtensionPoint.class)

A full example can be found in our example repository.

Building extension plugins

The use of extensions requires additional build steps. Make sure to use the ExtensionAnnotationProcessor in the compile plugin.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
        <verbose>true</verbose>
        <compilerVersion>1.8</compilerVersion>
        <source>1.8</source>
        <target>1.8</target>
        <annotationProcessors>
            <annotationProcessor>org.pf4j.processor.ExtensionAnnotationProcessor</annotationProcessor>
        </annotationProcessors>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.pf4j</groupId>
            <artifactId>pf4j</artifactId>
            <version>3.0.1</version>
        </dependency>
    </dependencies>
</plugin>

Additionally the plugin which provides the extension muse have an <Plugin-Dependencies> manifest entry that contains the consuming plugin id.

Make sure to share the extension point interfaces via a common maven module.

Integration Testing

Plugins can be directly tested in your IDE by starting an embedded Gentics Mesh instance. We provide a JUnit class rule which can be used to quickly startup Gentics Mesh: OrientDBMeshLocalServer

@ClassRule
public static final MeshLocalServer server = new OrientDBMeshLocalServer()
    .withInMemoryMode()
    .withPlugin(GraphQLExamplePlugin.class, "myPlugin")
    .waitForStartup();

You need to following test dependencies in order to use the class rule.

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.gentics.mesh</groupId>
    <artifactId>mesh-test-common</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.gentics.mesh</groupId>
    <artifactId>mesh-core</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.gentics.mesh</groupId>
    <artifactId>mesh-mdm-orientdb-wrapper</artifactId>
    <scope>test</scope>
</dependency>

Full Example

This example shows how to test the GraphQL plugin using Mesh with the OrientDB storage option.

GraphQlExamplePluginTest.java
public class GraphQlExamplePluginTest {

	private static String PROJECT = "test";

	@ClassRule
	public static final MeshLocalServer server = new MeshLocalServer()
		.withInMemoryMode()
		.withPlugin(GraphQLExamplePlugin.class, "myPlugin")
		.waitForStartup();

	@Test
	public void testPlugin() throws IOException {
		MeshRestClient client = server.client();

		// Create the project
		Completable createProject = client.createProject(
			new ProjectCreateRequest()
				.setName(PROJECT)
				.setSchemaRef("folder"))
			.toCompletable();

		// Run query on the plugin
		String query = "{ pluginApi { myPlugin { text } } }";
		Single<GraphQLResponse> rxQuery = client.graphqlQuery(PROJECT, query)
			.toSingle();

		// Run the operations
		GraphQLResponse result = createProject.andThen(rxQuery).blockingGet();

		// Print the GraphQL API result
		System.out.println(result.getData().encodePrettily());
	}
}

Development Guide

You can follow the Plugin Development Guide for a step by step introduction.

Examples

The Plugin Examples demonstrates basic plugin usage and test setup.

Classloading

The plugin system relies on PF4J which uses a Parent Last classloader. This means that classes will be first loaded from the plugin even if they are also part of Gentics Mesh.

To avoid NoClassDefFoundError / NoSuchMethodError it is thus recommended to add all the classes (libraries) that are used by the plugin jar.

Vert.x

The mesh-plugin-api, vertx-core, vertx-web, netty libraries must not be included in the plugin jar. Adding those libraries could cause stability issues and errors.

Instead you must mark those dependencies as provided.

...
<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-core</artifactId>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-web</artifactId>
    <scope>provided</scope>
</dependency>
...

Troubleshoot

  1. I’m getting java.lang.NoSuchMethodException: com.gentics.mesh.plugin.MyPlugin.<init>(org.pf4j.PluginWrapper, com.gentics.mesh.plugin.env.PluginEnvironment)

    You get this error if you have not added the mandatory constructor to your plugin.

  2. I’m getting NoClassDefFoundError / NoSuchMethodError

    You deployed your plugin on a Gentics Mesh instance which is not compatible with the classes you are using. You can rebuild the plugin with the new version or try to include your dependencies in the plugin jar to avoid classloader issues. Please note that vertx-core, vertx-web, netty should not be included in the jar.

  3. I’m getting Caused by: java.lang.ClassCastException: io.vertx.core.logging.SLF4JLogDelegateFactory cannot be cast to io.vertx.core.spi.logging.LogDelegateFactory

    This error happens if you include vert-core in your plugin. Please ensure that you don’t include any vert.x dependency that is part of Gentics Mesh. Instead mark those as provided.

Contributing

You wrote a plugin? Great! Share it with the community via the Gentics Mesh Awesome List