Quarkus ArangoDB Client Extension

ArangoDB is a scalable graph database system to drive value from connected data, faster. Native graphs, an integrated search engine, and JSON support, via a single query language.

This extension has been developed following samples presents in ArangoDB Java Driver

Installation

If you want to use this extension, you need to add the io.quarkiverse.arangodb-client-ext:quarkus-arangodb-client-ext extension first to your build file.

For instance, with Maven, add the following dependency to your POM file:

<dependency>
    <groupId>io.quarkiverse.arangodb-client-ext</groupId>
    <artifactId>quarkus-arangodb-client-ext</artifactId>
    <version>1.0.1</version>
</dependency>

Configuring

The Arangodb driver can be configured with standard Quarkus properties.

Connection

Configuration support multiple hosts. If you have only one host you can default empty host naming can be used.

  1. Single Host configuration

    src/main/resources/application.properties
    quarkus.arangodb.hosts.hostname = localhost
    quarkus.arangodb.hosts.port = 8529
    src/main/resources/application.properties
    quarkus.arangodb.hosts.host1.hostname = host1
    quarkus.arangodb.hosts.host1.port = 8529
    quarkus.arangodb.hosts.host2.hostname = host2
    quarkus.arangodb.hosts.host2.port = 8529

Authentication

Standard username / password authentication

src/main/resources/application.properties
quarkus.arangodb.user = root
quarkus.arangodb.password = password

JWT authentication

ArangoDB Java Driver provide JWT based authentication. This kind of authentication is not implemented because no use case has been determined.

SSL secure connection

To activate ssl secure connection please add these properties:

src/main/resources/application.properties
quarkus.arangodb.use-ssl = true
quarkus.arangodb.ssl-truststore.location = example.truststore
quarkus.arangodb.ssl-truststore.password = 12345678
The truststore muse be generated using the PEM certificate used on Arangodb server side. The truststore location can refer to resources folder or on an external one. It is preferable to use an external one to better handle certificate renewal or revocation without repackaging the application.
If you want to add, setup SSL using a PEM certificate on Arangodb side you could have a look to ArangodbContainer inside this project. It can give you good insights for producing custom container image regarding embedding certificate and updating arangodb.conf. You can easily produce an image based on official arangodb image and use /docker-entrypoint-initdb.d/ folder to add your custom shell or javascript scripts.
PEM certificate and example.truststore using password 12345678 used in this extension are coming from the ArangoDB Java Driver

Dev Services

Quarkus supports a feature called Dev Services that allows you to create various datasources without any config. Dev Services will bring up an Arangodb container if you didn’t explicit add the default values or configured custom values for any of quarkus.arangodb.hosts.*, quarkus.arangodb.user or quarkus.arangodb.password.

Otherwise, Quarkus will automatically start an Arangodb container when running tests or dev-mode, and automatically configure the connection.

When running the production version of the application, the Arangodb connection need to be configured as normal, so if you want to include a production database config in your application.properties and continue to use Dev Services we recommend that you use the %prod. profile to define your Neo4j settings.

Customization

For the following paragraph the Person object will be used as sample to describe many ways to use, implement custom serializer / deserializer.

import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public final class Person {
    private final String name;

    @JsonCreator
    public Person(@JsonProperty("name") final String name) {
        this.name = Objects.requireNonNull(name);
    }

    public String getName() {
        return name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;
        Person person = (Person) o;
        return Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
}

Serializer / Deserializer

By default, if jackson is not added as a dependency in the application, the shaded Jackson version present inside the driver will be used. In this case the configuration will be automatic. It works fine, however custom ObjectMapper configuration is not supported.

To define custom Jackson configuration you can do it in several ways.

  1. Using Quarkus jackson

    Add this dependency in pom.xml

    pom.xml
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-jackson</artifactId>
    </dependency>

    Next you can define a custom ObjectMapperCustomizer. This customizer is common across all the application.

    import java.io.IOException;
    
    import jakarta.inject.Singleton;
    
    import com.fasterxml.jackson.core.JsonGenerator;
    import com.fasterxml.jackson.core.JsonParser;
    import com.fasterxml.jackson.databind.*;
    import com.fasterxml.jackson.databind.module.SimpleModule;
    
    import io.quarkus.jackson.ObjectMapperCustomizer;
    
    @Singleton
    public final class RegisterCustomModuleCustomizer implements ObjectMapperCustomizer {
        private static final String PERSON_SERIALIZER_ADDED_PREFIX = "MyNameIs";
        private static final String PERSON_DESERIALIZER_ADDED_PREFIX = "Hello";
    
        @Override
        public void customize(final ObjectMapper mapper) {
            final SimpleModule module = new SimpleModule("PersonModule");
            module.addDeserializer(Person.class, new PersonDeserializer());
            module.addSerializer(Person.class, new PersonSerializer());
            mapper.registerModule(module);
        }
    
        private static class PersonSerializer extends JsonSerializer<Person> {
            @Override
            public void serialize(final Person value, final JsonGenerator gen, final SerializerProvider serializers)
                    throws IOException {
                gen.writeStartObject();
                gen.writeFieldName("name");
                gen.writeString(PERSON_SERIALIZER_ADDED_PREFIX + value.getName());
                gen.writeEndObject();
            }
        }
    
        private static class PersonDeserializer extends JsonDeserializer<Person> {
            @Override
            public Person deserialize(final JsonParser parser, final DeserializationContext ctx) throws IOException {
                final JsonNode rootNode = parser.getCodec().readTree(parser);
                final JsonNode nameNode = rootNode.get("name");
                final String name;
                if (nameNode != null && nameNode.isTextual()) {
                    name = PERSON_DESERIALIZER_ADDED_PREFIX + nameNode.asText();
                } else {
                    name = null;
                }
                return new Person(name);
            }
        }
    }
  2. Custom ArangodbSerde

    If you want to define ObjectMapper behaviors only for Arangodb you can do it by providing a custom ArangoSerde. Only one can be defined on the application.

    import static com.fasterxml.jackson.databind.DeserializationFeature.USE_BIG_INTEGER_FOR_INTS;
    import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED;
    
    import java.io.IOException;
    
    import jakarta.enterprise.inject.Produces;
    import jakarta.inject.Singleton;
    
    import com.arangodb.ContentType;
    import com.arangodb.serde.ArangoSerde;
    import com.arangodb.serde.jackson.JacksonSerde;
    import com.fasterxml.jackson.core.JsonGenerator;
    import com.fasterxml.jackson.core.JsonParser;
    import com.fasterxml.jackson.databind.*;
    import com.fasterxml.jackson.databind.module.SimpleModule;
    
    @Singleton
    public final class CustomArangodbSerde {
        private static final String PERSON_SERIALIZER_ADDED_PREFIX = "MyNameIs";
        private static final String PERSON_DESERIALIZER_ADDED_PREFIX = "Hello";
    
        @Singleton
        @Produces
        public ArangoSerde arangodbSerdeProducer() {
            return JacksonSerde.of(ContentType.JSON)
                    .configure(mapper -> {
                        mapper.configure(WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED, true);
                        mapper.configure(USE_BIG_INTEGER_FOR_INTS, true);
                        final SimpleModule module = new SimpleModule("PersonModule");
                        module.addDeserializer(Person.class, new CustomArangodbSerde.PersonDeserializer());
                        module.addSerializer(Person.class, new CustomArangodbSerde.PersonSerializer());
                        mapper.registerModule(module);
                    });
        }
    
        private static class PersonSerializer extends JsonSerializer<Person> {
            @Override
            public void serialize(final Person value, final JsonGenerator gen, final SerializerProvider serializers)
                    throws IOException {
                gen.writeStartObject();
                gen.writeFieldName("name");
                gen.writeString(PERSON_SERIALIZER_ADDED_PREFIX + value.getName());
                gen.writeEndObject();
            }
        }
    
        private static class PersonDeserializer extends JsonDeserializer<Person> {
            @Override
            public Person deserialize(final JsonParser parser, final DeserializationContext ctx) throws IOException {
                final JsonNode rootNode = parser.getCodec().readTree(parser);
                final JsonNode nameNode = rootNode.get("name");
                final String name;
                if (nameNode != null && nameNode.isTextual()) {
                    name = PERSON_DESERIALIZER_ADDED_PREFIX + nameNode.asText();
                } else {
                    name = null;
                }
                return new Person(name);
            }
        }
    }

SSLContext

By default, the SSLContext implementation use this example SslExampleTest defined in ArangoDB Java Driver.

It is convenient for most SSL use case. It can use certificate embedded inside the application or outside.

If you want to provide your own implementation returning the SSLContext you can do it by implementing your own ArangodbSSLContextProvider.

import javax.net.ssl.SSLContext;

import jakarta.enterprise.inject.Produces;
import jakarta.inject.Singleton;

import io.quarkiverse.arangodb.client.ext.runtime.ArangodbClientConfig;
import io.quarkiverse.arangodb.client.ext.runtime.ArangodbSSLContextException;
import io.quarkiverse.arangodb.client.ext.runtime.ArangodbSSLContextProvider;

@Singleton
public final class CustomArangodbSSLContextProviderProducer {
    static final class CustomArangodbSSLContextProvider implements ArangodbSSLContextProvider {

        @Override
        public SSLContext provide() throws ArangodbSSLContextException {
            throw new RuntimeException("TODO");
        }

    }

    @Singleton
    @Produces
    public ArangodbSSLContextProvider customArangodbSSLContextProviderProducer(
            final ArangodbClientConfig arangodbClientConfig) {
        return new CustomArangodbSSLContextProvider();
    }
}

Extension Configuration Reference

Configuration property fixed at build time - All other configuration properties are overridable at runtime

Configuration property

Type

Default

communication protocol, possible values are: VST, HTTP_JSON, HTTP_VPACK, HTTP2_JSON, HTTP2_VPACK, (default: HTTP2_JSON)

Environment variable: QUARKUS_ARANGODB_PROTOCOL

vst, http-json, http-vpack, http2-json, http2-vpack

connection and request timeout (ms), (default 0, no timeout)

Environment variable: QUARKUS_ARANGODB_TIMEOUT

int

username for authentication, (default: root)

Environment variable: QUARKUS_ARANGODB_USER

string

required

password for authentication

Environment variable: QUARKUS_ARANGODB_PASSWORD

string

required

use SSL connection, (default: false)

Environment variable: QUARKUS_ARANGODB_USE_SSL

boolean

false

enable hostname verification, (HTTP only, default: true)

Environment variable: QUARKUS_ARANGODB_VERIFY_HOST

boolean

VST chunk size in bytes, (default: 30000)

Environment variable: QUARKUS_ARANGODB_CHUNK_SIZE

int

max number of connections per host, (default: 1 VST, 1 HTTP/2, 20 HTTP/1.1)

Environment variable: QUARKUS_ARANGODB_MAX_CONNECTIONS

int

max lifetime of a connection (ms), (default: no ttl)

Environment variable: QUARKUS_ARANGODB_CONNECTION_TTL

long

VST keep-alive interval (s), (default: no keep-alive probes will be sent)

Environment variable: QUARKUS_ARANGODB_KEEP_ALIVE_INTERVAL

int

acquire the list of available hosts, (default: false)

Environment variable: QUARKUS_ARANGODB_ACQUIRE_HOST_LIST

boolean

acquireHostList interval (ms), (default: 3_600_000, 1 hour)

Environment variable: QUARKUS_ARANGODB_ACQUIRE_HOST_LIST_INTERVAL

int

load balancing strategy, possible values are: NONE, ROUND_ROBIN, ONE_RANDOM, (default: NONE)

Environment variable: QUARKUS_ARANGODB_LOAD_BALANCING_STRATEGY

none, round-robin, one-random

amount of samples kept for queue time metrics, (default: 10)

Environment variable: QUARKUS_ARANGODB_RESPONSE_QUEUE_TIME_SAMPLES

int

list of hosts to connect on

Type

Default

host hostname

Environment variable: QUARKUS_ARANGODB_HOSTS_HOSTNAME

string

required

host port

Environment variable: QUARKUS_ARANGODB_HOSTS_PORT

int

required

sslTruststore configuration This configuration section is optional

Type

Default

location where to find the cert file

Environment variable: QUARKUS_ARANGODB_SSL_TRUSTSTORE_LOCATION

path

required

trustStore password

Environment variable: QUARKUS_ARANGODB_SSL_TRUSTSTORE_PASSWORD

string

required