Maven project descriptor
In this guide you will learn how to create a plugin for Gentics Mesh which will extend the GraphQL API in order query a small library inventory.
This example is inspired by the GraphQL Java Getting Started Guide.
All sources for this example are available on GitHub.
Before we can start you need to install Apache Maven and a Java Development Kit.
Plugin Code:
pom.xml
Maven project descriptor
src/main/resources/schema.graphqls
GraphQL Schema
src/main/java/com/gentics/mesh/plugin/GraphQLDataFetchers.java
Data fetchers for the inventory data
src/main/java/com/gentics/mesh/plugin/GraphQLExamplePlugin.java
The actual plugin implementation
Test Code:
src/test/java/com/gentics/mesh/plugin/GraphQlExamplePluginTest.java
Plugin integration test
src/test/java/com/gentics/mesh/plugin/PluginRunnerExample.java
Example runner which will start Mesh and the plugin
/pom.xml
The first thing we need to do is to setup our project. In this example we’ll use the pom.xml
file which is used by Apache Maven to build the plugin project.
The pom.xml
will also contain plugin metadata. It is the place where the plugin manifest will be specified that is later added to the final .jar
file.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gentics.mesh.plugin</groupId>
<artifactId>mesh-graphql-library-example-plugin</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mesh.version>0.38.0</mesh.version>
<!-- We use these properties to specify the plugin manifest -->
<plugin.id>library</plugin.id>
<plugin.name>The GraphQL example plugin</plugin.name>
<plugin.description>A very simple plugin which shows how to extend the
GraphQL API.</plugin.description>
<plugin.class>com.gentics.mesh.plugin.GraphQLExamplePlugin</plugin.class>
<plugin.version>${project.version}</plugin.version>
<plugin.license>Apache License 2.0</plugin.license>
<plugin.author>Joe Doe</plugin.author>
<plugin.inception>2019-07-08</plugin.inception>
</properties>
<!-- The dependency management section provides the versions for the needed artifacts -->
<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>
<dependencies>
<!-- The mesh plugin api dependencies.
Please note that these dependencies need to be set to provided -->
<dependency>
<groupId>com.gentics.mesh</groupId>
<artifactId>mesh-plugin-dep</artifactId>
<scope>provided</scope>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<verbose>true</verbose>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<!--The shade plugin will generate the jar with all the needed dependencies -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<!-- We use the transformer to add the manifest properties to the jar manifest -->
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Plugin-Id>${plugin.id}</Plugin-Id>
<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>
</plugins>
</build>
<!-- The repository section is important.
Otherwise you will not be able to download the artifacts -->
<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>
</project>
The schema.graphqls
file contains the GraphQL Schema definition (SDL) which is used to setup the types for the inventory. It defines fields for each type. In our case we only need the Book
and the Author
types.
type Query {
bookById(id: ID): Book
}
type Book {
id: ID
name: String
pageCount: Int
author: Author
}
type Author {
id: ID
firstName: String
lastName: String
}
GraphQLExamplePlugin.java
The GraphQLExamplePlugin
file contains the actual plugin code. The plugin itself is very simple.
We need to extend the AbstractPlugin
class and implement the GraphQLPlugin
interface since our plugin will extend the GraphQL API.
In the initialize()
method we setup the GraphQLSchema
for our plugin. The counterpart is the shutdown()
method.
package com.gentics.mesh.plugin;
import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;
import java.net.URL;
import org.pf4j.PluginWrapper;
import com.gentics.mesh.plugin.env.PluginEnvironment;
import com.gentics.mesh.plugin.graphql.GraphQLPlugin;
import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import io.reactivex.Completable;
public class GraphQLExamplePlugin extends AbstractPlugin implements GraphQLPlugin {
private GraphQLSchema schema;
private GraphQLDataFetchers fetcher = new GraphQLDataFetchers();
public GraphQLExamplePlugin(PluginWrapper wrapper, PluginEnvironment env) {
super(wrapper, env);
}
@Override
public Completable initialize() {
return Completable.fromAction(() -> {
URL url = Resources.getResource("schema.graphqls");
String sdl = Resources.toString(url, Charsets.UTF_8);
schema = buildSchema(sdl);
});
}
private GraphQLSchema buildSchema(String sdl) {
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
RuntimeWiring runtimeWiring = buildWiring();
SchemaGenerator schemaGenerator = new SchemaGenerator();
return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
}
private RuntimeWiring buildWiring() {
return RuntimeWiring.newRuntimeWiring()
.type(newTypeWiring("Query")
.dataFetcher("bookById", fetcher.getBookByIdDataFetcher()))
.type(newTypeWiring("Book")
.dataFetcher("author", fetcher.getAuthorDataFetcher()))
.build();
}
@Override
public GraphQLSchema createRootSchema() {
return schema;
}
}
The data fetcher contains the code that is used to actually retrieve the inventory information. One fetcher is used for looking up books by id. The other one will return the author.
package com.gentics.mesh.plugin;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.google.common.collect.ImmutableMap;
import graphql.schema.DataFetcher;
public class GraphQLDataFetchers {
private static List<Map<String, String>> books = Arrays.asList(
ImmutableMap.of("id", "book-1",
"name", "Harry Potter and the Philosopher's Stone",
"pageCount", "223",
"authorId", "author-1"),
ImmutableMap.of("id", "book-2",
"name", "Moby Dick",
"pageCount", "635",
"authorId", "author-2"),
ImmutableMap.of("id", "book-3",
"name", "Interview with the vampire",
"pageCount", "371",
"authorId", "author-3"));
private static List<Map<String, String>> authors = Arrays.asList(
ImmutableMap.of("id", "author-1",
"firstName", "Joanne",
"lastName", "Rowling"),
ImmutableMap.of("id", "author-2",
"firstName", "Herman",
"lastName", "Melville"),
ImmutableMap.of("id", "author-3",
"firstName", "Anne",
"lastName", "Rice"));
public DataFetcher getBookByIdDataFetcher() {
return dataFetchingEnvironment -> {
String bookId = dataFetchingEnvironment.getArgument("id");
return books
.stream()
.filter(book -> book.get("id").equals(bookId))
.findFirst()
.orElse(null);
};
}
public DataFetcher getAuthorDataFetcher() {
return dataFetchingEnvironment -> {
Map<String, String> book = dataFetchingEnvironment.getSource();
String authorId = book.get("authorId");
return authors
.stream()
.filter(author -> author.get("id").equals(authorId))
.findFirst()
.orElse(null);
};
}
}
For testing it is possible to use the MeshLocalServer
class rule which will make it possible to start-up an in-memory Gentics Mesh server that can be used to run your plugin.
The test will:
Start Gentics Mesh in memory
Deploy the plugin
Setup an empty project
Finally run the the query to fetch the data from the plugin
package com.gentics.mesh.plugin;
import java.io.IOException;
import org.junit.ClassRule;
import org.junit.Test;
import com.gentics.mesh.core.rest.graphql.GraphQLResponse;
import com.gentics.mesh.core.rest.project.ProjectCreateRequest;
import com.gentics.mesh.rest.client.MeshRestClient;
import com.gentics.mesh.test.local.MeshLocalServer;
import io.reactivex.Completable;
import io.reactivex.Single;
/**
* This example test demonstrates how to use the {@link MeshLocalServer} to deploy a plugin.
*/
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());
}
}
When running the test you should see the output in the result of the GraphQL test request.
INFO: 127.0.0.1 - POST /api/v1/test/graphql HTTP/1.1 200 106 - 12 ms
{
"pluginApi" : {
"myPlugin" : {
"bookById" : {
"id" : "book-1",
"name" : "Harry Potter and the Philosopher's Stone"
}
}
}
}
Now we can package the plugin by running mvn clean package
. The built plugin will be placed in the target
directory.
We can now place the mesh-graphql-library-example-plugin-0.0.1-SNAPSHOT.jar
file in the Gentics Mesh plugins
directory.
You can now deploy start a new Gentics Mesh instance which will deploy the plugin during start-up:
docker run --rm \
-v $PWD/target/mesh-graphql-library-example-plugin-0.0.1-SNAPSHOT.jar:/plugins/graphql.jar \
-p 8080:8080 \
gentics/mesh:0.38.0
It is also possible to deploy a plugin in a running instance via the /admin/plugins
endpoint.
With an admin token at hand you can invoke the deployment via cURL:
curl -X POST \
-d '{
"path":"mesh-graphql-library-example-plugin-0.0.1-SNAPSHOT.jar"
}' \
-H "Authorization: Bearer $MESH_TOKEN" \
-H "Content-Type: application/json" \
$MESH_BASE/admin/plugins
You can read more about how to use the REST API with cURL in the API guide.
Once the plugin has been deployed you can use the plugin via the GraphQL endpoint.
curl -X POST \
-d '{
"query":"{ pluginApi { myPlugin { bookById(id: \"book-1\") {id name } } } }"
}' \
-H "Content-Type: application/json" \
$MESH_BASE/test/graphql