Building a Java headless CMS in 2018 using Vert.x

by

Johannes Schüth

Lead Developer of Gentics Mesh

Gentics Mesh

  • Headless CMS

  • Gentics builds CMS since 2000

  • Started in 11/2014

  • First public release 04/2016

  • Open source since 07/2016

github graph

Headless CMS - Intro

  • back-end only CMS

  • Access via REST API or GraphQL

headless cms

Headless CMS - Requirements

  • multi language support

  • binary storage

  • image resizing

  • manage contents in tree structures

  • link handling

  • versioning

  • searchable contents

Gentics Mesh Technology Stack

Technology Stack - Requirements

  • Lightweight and modern web framework

  • Embeddable Graph Database

  • Contents must be searchable

  • Scalability

Technology Stack - 2018

  • Eclipse Vert.x

  • OrientDB Graph Database

  • Dagger2

  • RxJava2

  • GraphQL

  • Elasticsearch

  • Hazelcast

Eclipse Vert.x

Eclipse Vert.x - Intro

  • Swiss army knife for HTTP

  • Reactive Framework

  • Async API (Datastorage, Filesystem, HTTP)

  • Modular / General purpose

Eclipse Vert.x - Web

  • HTTP routing

  • Session, Cookie, Upload handling

  • Websockets

  • Authentication handlers

Vertx vertx = Vertx.vertx();

Router router = Router.router(vertx);

router.route("/hello").handler(rc -> {
	rc.response().end("World");
});

vertx.createHttpServer()
	.requestHandler(router::accept)
	.listen(8080);

Eclipse Vert.x - Pitfalls #1

  • Vert.x is very flexible

  • Multiple ways to setup a REST API project

Eclipse Vert.x - Pitfalls #1

  • Routes are setup once and shared for all verticles

vertx rest setup wrong

Eclipse Vert.x - Pitfalls #1

  • Routes are setup once and shared for all verticles

vertx rest setup wrong cross

Eclipse Vert.x - Pitfalls #1

vertx rest setup

RxJava2

RxJava2

  • Reactive Extensions for the JVM

RxJava2 - Chaining Requests

MeshRestClient client = MeshRestClient.create("localhost", Vertx.vertx());

List<String> names = Arrays.asList("Iron Man",
	"Captain America", "Star Lord",
	"Black Widow", "Hulk");

client.findProjectByName("MCU").toSingle().flatMapCompletable(project -> {
	return Observable.fromIterable(names).flatMapSingle(name -> {

		NodeCreateRequest request = new NodeCreateRequest();
		request.setParentNodeUuid(project.getRootNode().getUuid());
		request.setLanguage("en");

		return client.createNode("MCU", request).toSingle();
	}).ignoreElements();
}).subscribe();

GraphQL

GraphQL - Intro

  • Query language used to query nested data structures

  • First released 2015

  • Developed by Facebook

  • Alternative to REST

GraphQL - Query

{
  node(path: "/aircrafts/space-shuttle") {
    uuid
    fields {
      ... on vehicle {
        weight
        price
        slug
      }
    }
  }
}
{
  "data": {
    "node": {
      "uuid": "f915b16fa68f40e395b16fa68f10e32d",
      "fields": {
        "weight": 22700,
        "price": 192000000000,
        "slug": "space-shuttle"
      }
    }
  }
}

GraphQL - Choice

Solves problems

  • Overfetching

  • Underfetching

  • Loading deeply nested data structures

GraphQL - Overfetching

The response of the server contains more information then you need

  • → Superfluous data still needs to be loaded to complete the request

  • → Causes additional delays and load on the system

GraphQL - Underfetching

The response of the server is lacking information you need

  • → Execute additional requests to load the data

  • → Causes additional delays

GraphQL - Pitfalls

  • Caching responses is not that easy

  • Error handling done via JSON

  • Not easy to create Types/POJOs for results

GraphQL - GraphQL-Java

  • Java API to create GraphQL schema

  • Github: github.com/graphql-java/graphql-java

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java</artifactId>
    <version>8.0</version>
</dependency>

GraphQL - Domain Model

The Conference references a Workshop.

public class Conference {

	private final Workshop workshop;

	public Conference(Workshop workshop) {
		this.workshop = workshop;
	}

	public Workshop getWorkshop() {
		return workshop;
	}
}

GraphQL - Domain Model

A Workshop has an id and a name.

public class Workshop {

	private final String name;
	private final long id;

	public Workshop(long id, String name) {
		this.id = id;
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public long getId() {
		return id;
	}
}

GraphQL - Schema #1

public static GraphQLSchema createSchema() {
	return GraphQLSchema.newSchema()
		.query(createQueryType())
		.build();
}
private static GraphQLObjectType createQueryType() {
	return newObject().name("QueryType")

		// .workshop
		.field(newFieldDefinition().name("workshop")
			.type(createWorkshopType())
			.dataFetcher((env) -> {
				Conference root = env.getSource();
				return root.getWorkshop();
			}))

		.build();
}

GraphQL - Schema #2

private static GraphQLObjectType createWorkshopType() {
	return newObject().name("Workshop")
		.description("A workshop element")

		// .id
		.field(newFieldDefinition().name("id")
			.description("The id of the workshop.")
			.type(new GraphQLNonNull(GraphQLLong)))

		// .name
		.field(newFieldDefinition().name("name")
			.description("The name of the workshop.")
			.type(GraphQLString))

		.build();
}

GraphQL - Query

{
    workshop {
        name, id
    }
}
GraphQLSchema schema = ConferenceSchema.createSchema();

Workshop demo = new Workshop(42, "Gentics Mesh Tech Stack");
Conference rootElement = new Conference(demo);

// Execute the query
GraphQL graphQL = newGraphQL(schema).build();
String operation = null;
Map<String, Object> variables = null;
ExecutionInput in =
	new ExecutionInput(query, operation, queryJson, rootElement, variables);

ExecutionResult result = graphQL.execute(in);
Map<String, Object> data = (Map<String, Object>) result.getData();

System.out.println(new JsonObject(data).encodePrettily());

GraphQL - GraphDB

  • Root element of the graph is starting point of query

  • Data fetchers can load directly referenced data

…
.dataFetcher((env) -> {
    RootElement root = env.getSource();
    return root.getDemo();
}))
…
  • Data fetchers directly traverses the GraphDB

  • Highly efficient

  • Convenient for profiling

GraphQL - Profiling

  • CPU profiling very expressive

  • Easy to spot potential bottlenecks

  • JProfiler example

graphql profiling

GraphQL - Examples

GraphDB

GraphDB - Stack

graph stack

GraphDB - Apache Tinkerpop

  • Open Source

  • Vendor Agnostic API

  • Supported by many graph database vendors

  • Gremlin Traversal Language

TinkerGraph graph = TinkerGraph.open();
GraphTraversalSource g = graph.traversal();

Vertex johannes = g.addV("name", "johannes").next();
Vertex tinkerpop = g.addV("name", "tinkerpop").next();
Edge egde = johannes.addEdge("presents", tinkerpop);

Ferma - Intro

  • Object Graph Mapper library (OGM)

  • Provides Java API to model your Graph Domain using classes

Ferma - Mesh Domain Model

domain model

GraphDB - OrientDB

  • Java based Graph Database

  • First release in 2010

  • Directly supports Tinkerpop API

  • Supports Master/Master replication

Dagger2 - Dependency Injection

Dagger2 - Intro

  • Dagger is a fully static, compile-time dependency injection framework for both Java.

  • Now maintained by Google (previously Square)

Dagger2 - Dependency Injection

  • Loosely coupled implementations

  • Provide dependencies in your code

  • Manage dependency hierarchies

Dagger2 - Java #1

The component describes the root of the dependency tree

@Singleton
@Component(modules = { AppModule.class })
public interface AppComponent {

	JsonObject configuration();

	HelloService hello();

	@Component.Builder
	interface Builder {
		@BindsInstance
		Builder configuration(JsonObject configuration);

		AppComponent build();
	}

}

Dagger2 - Java #2

@Singleton
public class HelloService {

	@Inject
	JsonObject configuration;

	@Inject
	public HelloService() {
	}

	public String getResult() {
		return configuration.getString("hello");
	}

}

Dagger2 - Java #3

  • AppComponent → DaggerAppComponent (generated)

JsonObject config = new JsonObject();
config.put("hello", "world");
AppComponent app = DaggerAppComponent.builder()
	.configuration(config)
	.build();

System.out.println(app.hello().getResult());

Dagger2 - Java #4

<dependencies>
    <dependency>
        <groupId>com.google.dagger</groupId>
        <artifactId>dagger-compiler</artifactId>
        <version>${dagger.version}</version>
    </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>
                <forceJavacCompilerUse>true</forceJavacCompilerUse>
            </configuration>
            <dependencies>
                <dependency>
                    <groupId>com.google.dagger</groupId>
                    <artifactId>dagger-compiler</artifactId>
                    <version>${dagger.version}</version>
                    <optional>true</optional>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

Dagger2 - Pitfalls

  • IDE integration can be tricky

dagger eclipse ide
  • Code regeneration

  • Source of dagger DI issues sometimes hard to trace

  • DI tree isolated

AppComponent app = DaggerAppComponent.builder()
	.configuration(config)
	.build();

Thank you



Examples on Github

Slides

Elasticsearch

Elasticsearch - Intro

  • Java based

  • Highly scaleable

  • Based on Apache Lucene

Elasticsearch - Mesh

  • Store Gentics Mesh elements in indices (Users, Nodes, Groups)

Elasticsearch - Pitfalls

  • Dynamic mapping can cause issues when input data is dynamic

  • Index types are gradually being deprecated

Elasticsearch - Syncing

Differential sync per index

  • Assign hashsum to each document

  • Collect hashsums of all elements in the graph

  • Collect hashsums of all elements in the index

Elasticsearch - Syncing

  • Compare using Google Guava:

  • com.google.common.collect.Maps#difference

Map<String, String> sourceVersions = loadVersionsFromGraph();

Map<String, String> sinkVersions = loadVersionsFromIndex(indexName);
MapDifference<String, String> diff = Maps.difference(sourceVersions,
                                       sinkVersions);

Set<String> needInsertionInES = diff.entriesOnlyOnLeft().keySet();
Set<String> needRemovalInES = diff.entriesOnlyOnRight().keySet();
Set<String> needUpdate = diff.entriesDiffering().keySet();

Thank you



Examples on Github

Slides