# Fauna v10 JVM client driver (current)
| Version: 1.0.0 | Repository: fauna/fauna-jvm |
| --- | --- | --- | --- |
Fauna’s JVM client driver lets you run FQL queries from Java and Scala applications.
This guide shows how to set up the driver and use it to run FQL queries.
## [](#requirements)Requirements
* Java 11 or later
## [](#supported-cloud-runtimes)Supported cloud runtimes
* AWS Lambda (See [AWS Lambda connections](#aws-lambda-connections))
## [](#installation)Installation
The driver is available on the [Maven central repository](https://central.sonatype.com/artifact/com.fauna/fauna-jvm). You can add the driver to your Java project using Gradle or Maven.
### [](#gradle)Gradle
File `build.gradle`:
```groovy
dependencies {
...
implementation "com.fauna:fauna-jvm:1.0.0"
...
}
```
### [](#maven)Maven
File `fauna-java/pom.xml`:
```xml
...
com.faunafauna-jvm1.0.0
...
```
## [](#api-reference)API reference
API reference documentation for the driver is available at [https://fauna.github.io/fauna-jvm/](https://fauna.github.io/fauna-jvm/).
## [](#sample-app)Sample app
For a practical example, check out the [Java sample app](https://github.com/fauna/java-sample-app).
This sample app is an e-commerce application that uses Spring Boot and the Fauna JVM driver. The source code includes comments highlighting best practices for using the driver and composing FQL queries.
## [](#basic-usage)Basic usage
The following application:
* Initializes a client instance to connect to Fauna.
* Composes a basic FQL query using an FQL string template.
* Runs the query using `query()` and `asyncQuery()`.
```java
package org.example;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import com.fauna.client.Fauna;
import com.fauna.client.FaunaClient;
import com.fauna.exception.FaunaException;
import com.fauna.query.builder.Query;
import com.fauna.response.QuerySuccess;
import com.fauna.types.Page;
import static com.fauna.codec.Generic.pageOf;
import static com.fauna.query.builder.Query.fql;
public class App {
// Define class for `Product` documents
// in expected results.
public static class Product {
public String name;
public String description;
public Integer price;
}
public static void main(String[] args) {
try {
// Initialize a default client.
// It will get the secret from the $FAUNA_SECRET environment variable.
FaunaClient client = Fauna.client();
// Compose a query.
Query query = fql("""
Product.sortedByPriceLowToHigh() {
name,
description,
price
}
""");
// Run the query synchronously.
System.out.println("Running synchronous query:");
runSynchronousQuery(client, query);
// Run the query asynchronously.
System.out.println("\nRunning asynchronous query:");
runAsynchronousQuery(client, query);
} catch (FaunaException e) {
System.err.println("Fauna error occurred: " + e.getMessage());
e.printStackTrace();
}
}
private static void runSynchronousQuery(FaunaClient client, Query query) throws FaunaException {
// Use `query()` to run a synchronous query.
// Synchronous queries block the current thread until the query completes.
// Accepts the query, expected result class, and a nullable set of query options.
QuerySuccess> result = client.query(query, pageOf(Product.class));
printResults(result.getData());
}
private static void runAsynchronousQuery(FaunaClient client, Query query) throws ExecutionException, InterruptedException {
// Use `asyncQuery()` to run an asynchronous, non-blocking query.
// Accepts the query, expected result class, and a nullable set of query options.
CompletableFuture>> futureResult = client.asyncQuery(query, pageOf(Product.class));
QuerySuccess> result = futureResult.get();
printResults(result.getData());
}
// Iterate through the products in the page.
private static void printResults(Page page) {
for (Product product : page.getData()) {
System.out.println("Name: " + product.name);
System.out.println("Description: " + product.description);
System.out.println("Price: " + product.price);
System.out.println("--------");
}
// Print the `after` cursor to paginate through results.
System.out.println("After: " + page.getAfter());
}
}
```
## [](#connect-to-fauna)Connect to Fauna
Each Fauna query is an independently authenticated request to the Core HTTP API’s [Query endpoint](../../../reference/http/reference/core-api/#operation/query). You authenticate with Fauna using an [authentication secret](../../../learn/security/authentication/#secrets).
### [](#get-an-authentication-secret)Get an authentication secret
Fauna supports several [secret types](../../../learn/security/authentication/#secret-types). For testing, you can create a [key](../../../learn/security/keys/), which is a type of secret:
1. Log in to the [Fauna Dashboard](https://dashboard.fauna.com/).
2. On the **Explorer** page, create a database.
3. In the database’s **Keys** tab, click **Create Key**.
4. Choose a **Role** of **server**.
5. Click **Save**.
6. Copy the **Key Secret**. The secret is scoped to the database.
### [](#initialize-a-client)Initialize a client
To send query requests to Fauna, initialize a `FaunaClient` instance with a Fauna authentication secret. You can pass the secret in a `FaunaConfig` object:
```java
FaunaConfig config = FaunaConfig.builder().secret("FAUNA_SECRET").build();
FaunaClient client = Fauna.client(config);
```
For supported properties, see [FaunaConfig.Builder](https://fauna.github.io/fauna-jvm/latest/com/fauna/client/FaunaConfig.Builder.html#%3Cinit%3E\(\)) in the API reference.
#### [](#use-an-environment-variable)Use an environment variable
If not specified, `secret` defaults to the `FAUNA_SECRET` environment variable. For example:
```java
// Defaults to the secret in the `FAUNA_SECRET` env var.
FaunaClient client = Fauna.client();
```
#### [](#connect-locally)Connect locally
The client comes with a helper config for connecting to Fauna running locally.
```java
// Connects to Fauna running locally via Docker (http://localhost:8443 and secret "secret").
FaunaClient local = Fauna.local();
```
#### [](#scoped-client)Scoped client
You can scope a client to a specific database and role. Scoped clients require a [key secret](../../../learn/security/keys/) with the built-in `admin` role. The driver uses this key to create a [scoped key](../../../learn/security/keys/#scoped-keys) internally.
```java
FaunaClient db1 = Fauna.scoped(client, FaunaScope.builder("Database1").build());
FaunaScope scope2 = FaunaScope.builder("Database2").withRole(FaunaRole.named("MyRole")).build();
FaunaClient db2 = Fauna.scoped(client, scope2);
```
### [](#multiple-connections)Multiple connections
You can use a single client instance to run multiple asynchronous queries at once. The driver manages HTTP connections as needed. Your app doesn’t need to implement connection pools or other connection management strategies.
You can create multiple client instances to connect to Fauna using different secrets or client configurations.
### [](#aws-lambda-connections)AWS Lambda connections
AWS Lambda freezes, thaws, and reuses execution environments for Lambda functions. See [Lambda execution environment](https://docs.aws.amazon.com/lambda/latest/dg/running-lambda-code.html).
When an execution environment is thawed, Lambda only runs the function’s handler code. Objects declared outside of the handler method remain initialized from before the freeze. Lambda doesn’t re-run initialization code outside the handler.
Fauna drivers keep socket connections that can time out during long freezes, causing `ECONNRESET` errors when thawed.
To prevent timeouts, create Fauna client connections inside function handlers. Fauna drivers use lightweight HTTP connections. You can create new connections for each request while maintaining good performance.
## [](#run-fql-queries)Run FQL queries
Use `fql` string templates to compose FQL queries. To run the query, pass the template and an expected result class to `query()` or `asyncQuery()`:
```java
Query query = fql("Product.sortedByPriceLowToHigh()");
QuerySuccess> result = client.query(query, pageOf(Product.class));
```
You can also pass a nullable set of [query options](#query-opts) to `query()` or `asyncQuery()`. These options control how the query runs in Fauna. See [Query options](#query-opts).
You can only compose FQL queries using string templates.
### [](#define-a-custom-class-for-your-data)Define a custom class for your data
Use annotations to map a Java class to a Fauna document or object shape:
```java
import com.fauna.annotation.FaunaField;
import com.fauna.annotation.FaunaId;
class Person {
@FaunaId
private String id;
private String firstName;
@FaunaField( name = "dob")
private String dateOfBirth;
}
```
You can use the `com.fauna.annotation` package to modify encoding and decoding of specific fields in classes used as arguments and results of queries:
* `@FaunaId`: Should only be used once per class and be associated with a field named `id` that represents the Fauna document ID. It’s not encoded unless the `isClientGenerated` flag is `true`.
* `@FaunaTs`: Should only be used once per class and be associated with a field named `ts` that represents the timestamp of a document. It’s not encoded.
* `@FaunaColl`: Typically goes unmodeled. Should only be used once per class and be associated with a field named `coll` that represents the collection field of a document. It will never be encoded.
* `@FaunaField`: Can be associated with any field to override its name in Fauna.
* `@FaunaIgnore`: Can be used to ignore fields during encoding and decoding.
Use classes in the `com.fauna.codec` package to handle type erasure when the top-level result of a query is a generic, including:
* `PageOf` where `T` is the element type.
* `ListOf` where `T` is the element type.
* `MapOf` where `T` is the value type.
* `OptionalOf` where `T` is the value type.
* `NullableDocumentOf` where `T` is the value type. This is specifically for cases when you return a Fauna document that may be null and want to receive a concrete `NullDocument` or `NonNullDocument` instead of catching a `NullDocumentException`.
### [](#var)Variable interpolation
Use `${}` to pass native Java variables to FQL. You can escape a variable by prepending an additional `$`.
```java
// Create a native Java var.
var collectionName = "Product";
// Pass the var to an FQL query.
Query query = fql("""
let collection = Collection(${collectionName})
collection.sortedByPriceLowToHigh()
""",
Map.of(
"collectionName", collectionName
));
```
The driver encodes interpolated variables to an appropriate [FQL type](../../../reference/fql/types/) and uses the [wire protocol](../../../reference/http/reference/wire-protocol/) to pass the query to the Core HTTP API’s [Query endpoint](../../../reference/http/reference/core-api/#operation/query). This helps prevent injection attacks.
### [](#query-composition)Query composition
You can use variable interpolation to pass FQL string templates as query fragments to compose an FQL query:
```java
// Create a reusable query fragment.
Query product = fql("Product.byName('pizza').first()");
// Prepare arguments for the query.
Map queryArgs = Map.of("product", product);
// Use the fragment in another FQL query.
Query query = fql("""
let product = ${product}
product {
name,
price
}
""", queryArgs);
```
## [](#pagination)Pagination
Use `paginate()` to asynchronously iterate through Sets that contain more than one page of results.
`paginate()` accepts the same [query options](#query-opts) as `query()` and `asyncQuery()`.
```java
import com.fauna.client.Fauna;
import com.fauna.client.FaunaClient;
import com.fauna.client.PageIterator;
public class App {
public static void main(String[] args) {
FaunaClient client = Fauna.client();
// Paginate will make an async request to Fauna.
PageIterator iter1 = client.paginate(fql("Product.all()"), Product.class);
// Handle each page. `PageIterator` extends the Java Iterator interface.
while (iter1.hasNext()) {
Page page = iter1.next();
List pageData = page.data();
// Do something with your data.
}
PageIterator iter2 = client.paginate(fql("Product.all()"), Product.class);
// You can use `flatten()` on `PageIterator` to iterate over every
// element in a Set.
Iterator productIter = iter2.flatten();
List products = new ArrayList<>();
// Iterate over Product elements without worrying about pages.
iter2.forEachRemaining((Product p) -> products.add(p));
}
}
```
## [](#query-stats)Query stats
Successful query responses and `ServiceException` exceptions include query stats:
```java
package org.example;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import com.fauna.client.Fauna;
import com.fauna.client.FaunaClient;
import com.fauna.exception.FaunaException;
import com.fauna.exception.ServiceException;
import com.fauna.query.builder.Query;
import static com.fauna.query.builder.Query.fql;
import com.fauna.response.QueryResponse;
import com.fauna.response.QuerySuccess;
public class App {
public static void main(String[] args) {
try {
FaunaClient client = Fauna.client();
Query query = fql("'Hello world'");
CompletableFuture> futureResponse = client.asyncQuery(query, String.class);
QueryResponse response = futureResponse.get();
System.out.println(response.getStats().toString());
} catch (FaunaException e) {
if (e instanceof ServiceException) {
ServiceException serviceException = (ServiceException) e;
System.out.println(serviceException.getStats().toString());
}
System.out.println(e);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
```
## [](#client-configuration)Client configuration
You can pass a `FaunaConfig` object to customize the configuration of a `FaunaClient` instance.
```java
FaunaConfig config = new FaunaConfig.Builder()
.secret("")
.build();
FaunaClient client = Fauna.client(config);
```
For properties, see [FaunaConfig.Builder](https://fauna.github.io/fauna-jvm/latest/com/fauna/client/FaunaConfig.Builder.html) in the API reference.
### [](#environment-variables)Environment variables
By default, `secret` and `endpoint` default to the respective `FAUNA_SECRET` and `FAUNA_ENDPOINT` environment variables.
For example, if you set the following environment variables:
```bash
export FAUNA_SECRET=FAUNA_SECRET
export FAUNA_ENDPOINT=https://db.fauna.com/
```
You can initialize the client with a default configuration:
```java
FaunaClient client = Fauna.client();
```
### [](#retries)Retries
The client automatically retries queries that receive a response with 429 HTTP status code. The client will retry a query up to 4 times, including the original query request. Retries use an exponential backoff.
## [](#query-opts)Query options
You can pass a `QueryOptions` object to `query()` or `asyncQuery()` to control how a query runs in Fauna. You can also use query options to instrument a query for monitoring and debugging.
```java
Query query = Query.fql("Hello World");
QueryOptions options = QueryOptions.builder()
.linearized(true)
.queryTags(Map.of("tag", "value"))
.timeout(Duration.ofSeconds(10))
.traceParent("00-750efa5fb6a131eb2cf4db39f28366cb-000000000000000b-00")
.typeCheck(false)
.build();
QuerySuccess result = client.query(query, String.class, options);
```
For properties, see [QueryOptions.Builder](https://fauna.github.io/fauna-jvm/latest/com/fauna/query/QueryOptions.Builder.html) in the API reference.
## [](#event-feeds)Event feeds
The driver supports [event feeds](../../../learn/cdc/). An event feed asynchronously polls an [event source](../../../learn/cdc/#create-an-event-source) for paginated events.
To use event feeds, you must have a Pro or Enterprise plan.
### [](#request-an-event-feed)Request an event feed
To get an event source, append [`set.eventSource()`](../../../reference/fql-api/set/eventsource/) or [`set.eventsOn()`](../../../reference/fql-api/set/eventson/) to a [supported Set](../../../learn/cdc/#sets).
To get an event feed, you can use one of the following methods:
* `feed()`: Synchronously fetches an event feed and returns a `FeedIterator` that you can use to iterate through the pages of events.
* `asyncFeed()`: Asynchronously fetches an event feed and returns a `CompletableFuture` that you can use to iterate through the pages of events.
* `poll()`: Asynchronously fetches a single page of events from the event feed and returns a `CompletableFuture` that you can use to handle each page individually. You can repeatedly call `poll()` to get successive pages.
You can use `flatten()` on a `FeedIterator` to iterate through events rather than pages.
```java
import com.fauna.client.Fauna;
import com.fauna.client.FaunaClient;
import com.fauna.event.FeedIterator;
import com.fauna.event.EventSource;
import com.fauna.event.FeedOptions;
import com.fauna.event.FeedPage;
import com.fauna.event.EventSource;
import com.fauna.response.QuerySuccess;
import com.fauna.event.FaunaEvent;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.CompletableFuture;
import static com.fauna.query.builder.Query.fql;
// Import the Product class for event data.
import org.example.Product;
public class EventFeedExample {
private static void printEventDetails(FaunaEvent event) {
System.out.println("Event Details:");
System.out.println(" Type: " + event.getType());
System.out.println(" Cursor: " + event.getCursor());
event.getTimestamp().ifPresent(ts ->
System.out.println(" Timestamp: " + ts)
);
event.getData().ifPresent(product ->
System.out.println(" Product: " + product.toString())
);
if (event.getStats() != null) {
System.out.println(" Stats: " + event.getStats());
}
if (event.getError() != null) {
System.out.println(" Error: " + event.getError());
}
System.out.println("-------------------");
}
public static void main(String[] args) {
FaunaClient client = Fauna.client();
long tenMinutesAgo = System.currentTimeMillis() * 1000 - (10 * 60 * 1000 * 1000);
FeedOptions options = FeedOptions.builder()
.startTs(tenMinutesAgo)
.pageSize(10)
.build();
// Example 1: Using `feed()`
FeedIterator syncIterator = client.feed(
fql("Product.all().eventsOn(.price, .stock)"),
options,
Product.class
);
System.out.println("----------------------");
System.out.println("`feed()` results:");
System.out.println("----------------------");
syncIterator.forEachRemaining(page -> {
for (FaunaEvent event : page.getEvents()) {
printEventDetails(event);
}
});
// Example 2: Using `asyncFeed()`
CompletableFuture> iteratorFuture = client.asyncFeed(
fql("Product.all().eventsOn(.price, .stock)"),
options,
Product.class
);
FeedIterator iterator = iteratorFuture.join();
System.out.println("----------------------");
System.out.println("`asyncFeed()` results:");
System.out.println("----------------------");
iterator.forEachRemaining(page -> {
for (FaunaEvent event : page.getEvents()) {
printEventDetails(event);
}
});
// Example 3: Using `flatten()` on a `FeedIterator`
FeedIterator flattenedIterator = client.feed(
fql("Product.all().eventSource()"),
options,
Product.class
);
Iterator> eventIterator = flattenedIterator.flatten();
List> allEvents = new ArrayList<>();
eventIterator.forEachRemaining(allEvents::add);
System.out.println("----------------------");
System.out.println("`flatten()` results:");
System.out.println("----------------------");
for (FaunaEvent event : allEvents) {
printEventDetails(event);
}
// Example 4: Using `poll()`
QuerySuccess sourceQuery = client.query(
fql("Product.all().eventSource()"),
EventSource.class
);
EventSource source = EventSource.fromResponse(sourceQuery.getData());
CompletableFuture> pageFuture = client.poll(
source,
options,
Product.class
);
while (pageFuture != null) {
FeedPage page = pageFuture.join();
List> events = page.getEvents();
System.out.println("----------------------");
System.out.println("`poll()` results:");
System.out.println("----------------------");
for (FaunaEvent event : events) {
printEventDetails(event);
}
if (page.hasNext()) {
FeedOptions nextPageOptions = options.nextPage(page);
pageFuture = client.poll(source, nextPageOptions, Product.class);
} else {
pageFuture = null;
}
}
}
}
```
If you pass an event source directly to `feed()` or `poll()` and changes occur between the creation of the event source and the event feed request, the feed replays and emits any related events.
In most cases, you’ll get events after a specific start time or cursor.
### [](#get-events-after-a-specific-start-time)Get events after a specific start time
When you first poll an event source using an event feed, you usually include a `startTs` (start timestamp) in the `FeedOptions` passed to `feed()`, `asyncFeed()`, or `poll()`.
`startTs` is an integer representing a time in microseconds since the Unix epoch. The request returns events that occurred after the specified timestamp (exclusive).
```java
Query query = fql("Product.all().eventsOn(.price, .stock)");
// Calculate the timestamp for 10 minutes ago in microseconds.
long tenMinutesAgo = System.currentTimeMillis() * 1000 - (10 * 60 * 1000 * 1000);
FeedOptions options = FeedOptions.builder()
.startTs(tenMinutesAgo)
.pageSize(10)
.build();
// Example 1: Using `feed()`
FeedIterator syncIterator = client.feed(
query,
options,
Product.class
);
// Example 2: Using `asyncFeed()`
CompletableFuture> iteratorFuture = client.asyncFeed(
query,
options,
Product.class
);
// Example 3: Using `poll()`
QuerySuccess sourceQuery = client.query(
query,
EventSource.class
);
EventSource source = EventSource.fromResponse(sourceQuery.getData());
CompletableFuture> pageFuture = client.poll(
source,
options,
Product.class
);
```
### [](#get-events-after-a-specific-cursor)Get events after a specific cursor
After the initial request, you usually get subsequent events using the cursor for the last page or event. To get events after a cursor (exclusive), include the cursor in the `FeedOptions` passed to `feed()`, `asyncFeed()`, or `poll()`:
```java
Query query = fql("Product.all().eventsOn(.price, .stock)");
FeedOptions options = FeedOptions.builder()
.cursor("gsGabc456") // Cursor for the last page
.pageSize(10)
.build();
// Example 1: Using `feed()`
FeedIterator syncIterator = client.feed(
query,
options,
Product.class
);
// Example 2: Using `asyncFeed()`
CompletableFuture> iteratorFuture = client.asyncFeed(
query,
options,
Product.class
);
// Example 3: Using `poll()`
QuerySuccess sourceQuery = client.query(
query,
EventSource.class
);
EventSource source = EventSource.fromResponse(sourceQuery.getData());
CompletableFuture> pageFuture = client.poll(
source,
options,
Product.class
);
```
### [](#error-handling)Error handling
Exceptions can be raised in two different places:
* While fetching a page
* While iterating a page’s events
This distinction lets ignore errors originating from event processing. For example:
```java
try {
FeedIterator syncIterator = client.feed(
fql("Product.all().map(.details.toUpperCase()).eventSource()"),
options,
Product.class
);
syncIterator.forEachRemaining(page -> {
try {
for (FaunaEvent event : page.getEvents()) {
// Event-specific handling
System.out.println("Event: " + event);
}
} catch (FaunaException e) {
// Handle errors for specific events within the page
System.err.println("Error processing event: " + e.getMessage());
}
});
} catch (FaunaException e) {
// Additional handling for initialization errors
System.err.println("Error occurred with event feed initialization: " + e.getMessage());
}
```
## [](#event-streaming)Event streams
The driver supports [event streams](../../../learn/cdc/).
To get an event source, append [`set.eventSource()`](../../../reference/fql-api/set/eventsource/) or [`set.eventsOn()`](../../../reference/fql-api/set/eventson/) to a [supported Set](../../../learn/cdc/#sets).
To start and subscribe to the stream, pass an `EventSource` and related `StreamOptions` to `stream()` or `asyncStream()`:
```java
// Get an event source.
Query query = fql("Product.all().eventSource() { name, stock }");
QuerySuccess tokenResponse = client.query(query, EventSource.class);
EventSource eventSource = EventSource.fromResponse(querySuccess.getData());
// Calculate the timestamp for 10 minutes ago in microseconds.
long tenMinutesAgo = System.currentTimeMillis() * 1000 - (10 * 60 * 1000 * 1000);
StreamOptions streamOptions = StreamOptions.builder().startTimestamp(tenMinutesAgo).build();
// Example 1: Using `stream()`
FaunaStream stream = client.stream(eventSource, streamOptions, Product.class);
// Example 2: Using `asyncStream()`
CompletableFuture> futureStream = client.asyncStream(source, streamOptions, Product.class);
```
If changes occur between the creation of the event source and the stream request, the stream replays and emits any related events.
Alternatively, you can pass an FQL query that returns an event source to `stream()` or `asyncStream()`:
```java
Query query = fql("Product.all().eventSource() { name, stock }");
// Example 1: Using `stream()`
FaunaStream stream = client.stream(query, Product.class);
// Example 2: Using `asyncStream()`
CompletableFuture> futureStream = client.asyncStream(query, Product.class);
```
### [](#create-a-subscriber-class)Create a subscriber class
The methods return a `FaunaStream` publisher that lets you handle events as they arrive. Create a class with the `Flow.Subscriber` interface to process events:
```java
package org.example;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicInteger;
import com.fauna.client.Fauna;
import com.fauna.client.FaunaClient;
import com.fauna.event.FaunaEvent;
import com.fauna.event.FaunaStream;
import com.fauna.exception.FaunaException;
import static com.fauna.query.builder.Query.fql;
// Import the Product class for event data.
import org.example.Product;
public class EventStreamExample {
public static void main(String[] args) throws InterruptedException {
try {
FaunaClient client = Fauna.client();
// Create a stream of all products. Project the name and stock.
FaunaStream stream = client.stream(fql("Product.all().eventSource() { name, stock }"), Product.class);
// Create a subscriber to handle stream events.
ProductSubscriber subscriber = new ProductSubscriber();
stream.subscribe(subscriber);
// Wait for the subscriber to complete.
subscriber.awaitCompletion();
} catch (FaunaException e) {
System.err.println("Fauna error occurred: " + e.getMessage());
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class ProductSubscriber implements Flow.Subscriber> {
private final AtomicInteger eventCount = new AtomicInteger(0);
private Flow.Subscription subscription;
private final int maxEvents;
private final CountDownLatch completionLatch = new CountDownLatch(1);
public ProductSubscriber() {
// Stream closes after 3 events.
this.maxEvents = 3;
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
subscription.request(1);
}
@Override
public void onNext(FaunaEvent event) {
// Handle each event...
int count = eventCount.incrementAndGet();
System.out.println("Received event " + count + ":");
System.out.println(" Type: " + event.getType());
System.out.println(" Cursor: " + event.getCursor());
System.out.println(" Timestamp: " + event.getTimestamp());
System.out.println(" Data: " + event.getData().orElse(null));
if (count >= maxEvents) {
System.out.println("Closing stream after " + maxEvents + " events");
subscription.cancel();
completionLatch.countDown();
} else {
subscription.request(1);
}
}
@Override
public void onError(Throwable throwable) {
System.err.println("Error in stream: " + throwable.getMessage());
completionLatch.countDown();
}
@Override
public void onComplete() {
System.out.println("Stream completed.");
completionLatch.countDown();
}
public int getEventCount() {
return eventCount.get();
}
public void awaitCompletion() throws InterruptedException {
completionLatch.await();
}
}
}
```
## [](#debug-logging)Debug logging
To log the driver’s HTTP requests and responses, set the `FAUNA_DEBUG` environment variable to `1`. The driver outputs requests and responses, including headers, to `stderr`. You can also use your logger.
Setting `Level.WARNING` is equivalent to `FAUNA_DEBUG=0`. Setting `Level.FINE` is equivalent to `FAUNA_DEBUG=1`. The driver logs HTTP request bodies at `Level.FINEST`.
```java
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.SimpleFormatter;
import com.fauna.client.Fauna;
import com.fauna.client.FaunaClient;
class App {
public static void main(String[] args) {
Handler handler = new ConsoleHandler();
handler.setLevel(Level.FINEST);
handler.setFormatter(new SimpleFormatter());
FaunaClient client = Fauna.client(FaunaConfig.builder().logHandler(handler).build());
}
}
```
# JVM driver source code
# Files
## File: src/main/java/com/fauna/annotation/FaunaColl.java
```java
package com.fauna.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Attribute used to indicate that a field should be ignored during encoding and decoding.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FaunaColl {
}
```
## File: src/main/java/com/fauna/annotation/FaunaField.java
```java
package com.fauna.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Attribute used to specify properties of a field in a Fauna object.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FaunaField {
String name() default "";
}
```
## File: src/main/java/com/fauna/annotation/FaunaFieldImpl.java
```java
package com.fauna.annotation;
@SuppressWarnings("ClassExplicitlyAnnotation")
public class FaunaFieldImpl implements FaunaField {
private final FaunaField annotation;
public FaunaFieldImpl(FaunaField annotation) {
this.annotation = annotation;
}
@Override
public String name() {
return (annotation != null && !annotation.name().isEmpty()) ?
annotation.name() : null;
}
@Override
public Class extends java.lang.annotation.Annotation> annotationType() {
return FaunaField.class;
}
}
```
## File: src/main/java/com/fauna/annotation/FaunaId.java
```java
package com.fauna.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Attribute used to indicate that the field is the Fauna ID.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FaunaId {
boolean isClientGenerate() default false;
}
```
## File: src/main/java/com/fauna/annotation/FaunaIdImpl.java
```java
package com.fauna.annotation;
@SuppressWarnings("ClassExplicitlyAnnotation")
public class FaunaIdImpl implements FaunaId {
private final FaunaId annotation;
public FaunaIdImpl(FaunaId annotation) {
this.annotation = annotation;
}
@Override
public Class extends java.lang.annotation.Annotation> annotationType() {
return FaunaId.class;
}
@Override
public boolean isClientGenerate() {
return annotation != null && annotation.isClientGenerate();
}
}
```
## File: src/main/java/com/fauna/annotation/FaunaIgnore.java
```java
package com.fauna.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Attribute used to indicate that a field should be ignored during encoding and decoding.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FaunaIgnore {
}
```
## File: src/main/java/com/fauna/annotation/FaunaObject.java
```java
package com.fauna.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Attribute used to indicate that a class represents a Fauna object.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Deprecated
public @interface FaunaObject {
}
```
## File: src/main/java/com/fauna/annotation/FaunaTs.java
```java
package com.fauna.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Attribute used to indicate that a field should be ignored during encoding and decoding.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FaunaTs {
}
```
## File: src/main/java/com/fauna/client/BaseFaunaClient.java
```java
package com.fauna.client;
import java.net.http.HttpClient;
import java.util.Objects;
/**
* FaunaClient is the main client for interacting with Fauna.
* It provides functionality to send queries and receive responses.
*/
public final class BaseFaunaClient extends FaunaClient {
private final HttpClient httpClient;
private final RequestBuilder baseRequestBuilder;
private final RequestBuilder streamRequestBuilder;
private final RequestBuilder feedRequestBuilder;
private final RetryStrategy retryStrategy;
/**
* Construct a new FaunaClient instance with the provided FaunaConfig and HttpClient. This allows
* the user to have complete control over HTTP Configuration, like timeouts, thread pool size,
* and so-on.
*
* @param faunaConfig The Fauna configuration settings.
* @param httpClient A Java HTTP client instance.
* @param retryStrategy An implementation of RetryStrategy.
*/
public BaseFaunaClient(final FaunaConfig faunaConfig,
final HttpClient httpClient, final RetryStrategy retryStrategy) {
super(faunaConfig.getSecret(), faunaConfig.getLogHandler(),
faunaConfig.getStatsCollector());
this.httpClient = httpClient;
if (Objects.isNull(faunaConfig)) {
throw new IllegalArgumentException("FaunaConfig cannot be null.");
} else if (Objects.isNull(httpClient)) {
throw new IllegalArgumentException("HttpClient cannot be null.");
} else {
this.baseRequestBuilder =
RequestBuilder.queryRequestBuilder(faunaConfig,
getLogger());
this.streamRequestBuilder =
RequestBuilder.streamRequestBuilder(faunaConfig,
getLogger());
this.feedRequestBuilder =
RequestBuilder.feedRequestBuilder(faunaConfig, getLogger());
}
this.retryStrategy = retryStrategy;
}
/**
* Construct a new FaunaClient instance with the provided FaunaConfig, using default HTTP config and retry
* strategy.
*
* @param faunaConfig The Fauna configuration settings.
*/
public BaseFaunaClient(final FaunaConfig faunaConfig) {
this(faunaConfig, HttpClient.newBuilder().build(),
DEFAULT_RETRY_STRATEGY);
}
RequestBuilder getRequestBuilder() {
return this.baseRequestBuilder;
}
RequestBuilder getStreamRequestBuilder() {
return this.streamRequestBuilder;
}
RequestBuilder getFeedRequestBuilder() {
return this.feedRequestBuilder;
}
HttpClient getHttpClient() {
return this.httpClient;
}
RetryStrategy getRetryStrategy() {
return this.retryStrategy;
}
}
```
## File: src/main/java/com/fauna/client/ExponentialBackoffStrategy.java
```java
package com.fauna.client;
/**
* Implements an exponential backoff strategy for retries.
* The backoff delay increases exponentially with each retry attempt, with optional jitter.
*/
public final class ExponentialBackoffStrategy implements RetryStrategy {
private final float backoffFactor;
private final int maxAttempts;
private final int initialIntervalMillis;
private final int maxBackoffMillis;
private final float jitterFactor;
/**
* Constructs an Exponential backoff strategy.
*
* @param maxAttempts The maximum number of retry attempts. Defaults to 3 retries.
* @param backoffFactor The factor by which the delay will increase. Default is 2.
* @param initialIntervalMillis The interval (in milliseconds) for the first retry attempt. Default is 1000ms.
* @param maxBackoffMillis The maximum delay (in milliseconds) between retries. Default is 20000ms.
* @param jitterFactor A value between 0 and 1 that controls the jitter factor. Default is 0.5.
*/
ExponentialBackoffStrategy(final int maxAttempts, final float backoffFactor,
final int initialIntervalMillis,
final int maxBackoffMillis, final float jitterFactor) {
this.maxAttempts = maxAttempts;
this.backoffFactor = backoffFactor;
this.initialIntervalMillis = initialIntervalMillis;
this.maxBackoffMillis = maxBackoffMillis;
this.jitterFactor = jitterFactor;
if (jitterFactor < 0.0 || jitterFactor > 1.0) {
throw new IllegalArgumentException("Jitter factor must be between 0 and 1.");
}
if (backoffFactor < 0.0) {
throw new IllegalArgumentException("Backoff factor must be positive.");
}
if (maxAttempts < 0) {
throw new IllegalArgumentException("Max attempts must be a natural number (not negative).");
}
if (initialIntervalMillis < 0) {
throw new IllegalArgumentException("Initial interval must be positive.");
}
if (maxBackoffMillis < 0) {
throw new IllegalArgumentException("Max backoff must be positive.");
}
}
/**
* Generates a random jitter percent between 0 and the jitterFactor.
*
* @return A random jitter percent.
*/
private double getJitterPercent() {
return Math.random() * jitterFactor;
}
@Override
public boolean canRetry(final int retryAttempt) {
if (retryAttempt < 0) {
throw new IllegalArgumentException("Retry attempt must be a natural number (not negative).");
}
return retryAttempt <= maxAttempts;
}
@Override
public int getDelayMillis(final int retryAttempt) {
if (retryAttempt < 0) {
throw new IllegalArgumentException("Retry attempt must be a natural number (not negative).");
} else if (retryAttempt == 0) {
return 0;
} else {
double deterministicBackoff = Math.pow(this.backoffFactor, retryAttempt - 1);
double calculatedBackoff = deterministicBackoff * (1 - getJitterPercent()) * initialIntervalMillis;
return (int) Math.min(calculatedBackoff, this.maxBackoffMillis);
}
}
@Override
public int getMaxRetryAttempts() {
return this.maxAttempts;
}
/**
* Builder class for the ExponentialBackoffStrategy.
* Allows fluent configuration of the backoff strategy parameters.
*/
public static class Builder {
private float backoffFactor = 2.0f;
private int maxAttempts = 3;
private int initialIntervalMillis = 1000;
private int maxBackoffMillis = 20_000;
private float jitterFactor = 0.5f;
/**
* Sets the maximum number of retry attempts.
*
* @param maxAttempts The maximum number of retry attempts.
* @return The current Builder instance.
*/
public Builder maxAttempts(final int maxAttempts) {
this.maxAttempts = maxAttempts;
return this;
}
/**
* Sets the backoff factor.
*
* @param backoffFactor The factor by which the backoff delay increases.
* @return The current Builder instance.
*/
public Builder backoffFactor(final float backoffFactor) {
this.backoffFactor = backoffFactor;
return this;
}
/**
* Sets the initial interval (in milliseconds) for the first retry attempt.
*
* @param initialIntervalMillis The initial interval in milliseconds.
* @return The current Builder instance.
*/
public Builder initialIntervalMillis(final int initialIntervalMillis) {
this.initialIntervalMillis = initialIntervalMillis;
return this;
}
/**
* Sets the maximum backoff (in milliseconds) between retries.
*
* @param maxBackoffMillis The maximum backoff in milliseconds.
* @return The current Builder instance.
*/
public Builder maxBackoffMillis(final int maxBackoffMillis) {
this.maxBackoffMillis = maxBackoffMillis;
return this;
}
/**
* Sets the jitter factor (between 0 and 1) to control how much to jitter the backoff delay.
*
* @param jitterFactor The jitter factor.
* @return The current Builder instance.
*/
public Builder jitterFactor(final float jitterFactor) {
this.jitterFactor = jitterFactor;
return this;
}
/**
* Builds and returns a new ExponentialBackoffStrategy instance.
*
* @return A new ExponentialBackoffStrategy.
*/
public ExponentialBackoffStrategy build() {
return new ExponentialBackoffStrategy(
this.maxAttempts, this.backoffFactor,
this.initialIntervalMillis,
this.maxBackoffMillis, this.jitterFactor);
}
}
/**
* Creates a new Builder instance for ExponentialBackoffStrategy.
*
* @return A new Builder instance.
*/
public static Builder builder() {
return new Builder();
}
}
```
## File: src/main/java/com/fauna/client/Fauna.java
```java
package com.fauna.client;
import java.net.http.HttpClient;
public final class Fauna {
private Fauna() {
}
/**
* Create a default Fauna client.
*
* @return A FaunaClient (or subclass of it).
*/
public static FaunaClient client() {
return new BaseFaunaClient(FaunaConfig.builder().build());
}
/**
* Create a Fauna client with the given FaunaConfig (and default HTTP client, and RetryStrategy).
*
* @param config Fauna configuration object.
* @return A FaunaClient (or subclass of it).
*/
public static FaunaClient client(final FaunaConfig config) {
if (config == null) {
throw new IllegalArgumentException("FaunaConfig cannot be null.");
}
return new BaseFaunaClient(config);
}
/**
* Create a Fauna client with the given FaunaConfig, HTTP client, and RetryStrategy.
*
* @param config Fauna configuration object.
* @param httpClient An HTTP client (from java.net.http in Java 11+).
* @param retryStrategy An implementation of RetryStrategy.
* @return A FaunaClient (or subclass of it).
*/
public static FaunaClient client(final FaunaConfig config, final HttpClient httpClient,
final RetryStrategy retryStrategy) {
if (config == null) {
throw new IllegalArgumentException("FaunaConfig cannot be null.");
}
return new BaseFaunaClient(config, httpClient, retryStrategy);
}
/**
* Create a new Fauna client that wraps an existing client, but is scoped to a specific database.
*
* @param client Another Fauna client.
* @param database The name of the database.
* @return A FaunaClient (or subclass of it).
*/
public static FaunaClient scoped(final FaunaClient client, final String database) {
if (client == null) {
throw new IllegalArgumentException("FaunaClient cannot be null.");
}
if (database == null || database.isEmpty()) {
throw new IllegalArgumentException(
"database cannot be null or empty.");
}
return new ScopedFaunaClient(client,
FaunaScope.builder(database).build());
}
/**
* Create a new Fauna client that wraps an existing client, but is scoped to a specific database.
*
* @param client Another Fauna client.
* @param database The name of the database.
* @param role A Fauna role (either built-in or user defined).
* @return A FaunaClient (or subclass of it).
*/
public static FaunaClient scoped(final FaunaClient client, final String database,
final FaunaRole role) {
if (client == null) {
throw new IllegalArgumentException("FaunaClient cannot be null.");
}
if (database == null || database.isEmpty()) {
throw new IllegalArgumentException(
"database cannot be null or empty.");
}
if (role == null) {
throw new IllegalArgumentException("role cannot be null or empty.");
}
return new ScopedFaunaClient(client,
FaunaScope.builder(database).withRole(role).build());
}
/**
* Create a Fauna client for local development using the Fauna Docker container.
*
* @return A FaunaClient (or subclass of it).
*/
public static FaunaClient local() {
return new BaseFaunaClient(FaunaConfig.LOCAL);
}
}
```
## File: src/main/java/com/fauna/client/FaunaClient.java
```java
package com.fauna.client;
import com.fauna.codec.Codec;
import com.fauna.codec.CodecProvider;
import com.fauna.codec.DefaultCodecProvider;
import com.fauna.codec.DefaultCodecRegistry;
import com.fauna.codec.ParameterizedOf;
import com.fauna.event.EventSource;
import com.fauna.event.FaunaStream;
import com.fauna.event.FeedIterator;
import com.fauna.event.FeedOptions;
import com.fauna.event.FeedPage;
import com.fauna.event.StreamOptions;
import com.fauna.exception.ClientException;
import com.fauna.exception.FaunaException;
import com.fauna.exception.ServiceException;
import com.fauna.query.AfterToken;
import com.fauna.query.QueryOptions;
import com.fauna.query.builder.Query;
import com.fauna.response.QueryResponse;
import com.fauna.response.QuerySuccess;
import com.fauna.types.Page;
import java.io.InputStream;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.text.MessageFormat;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import java.util.logging.Handler;
import java.util.logging.Logger;
import static com.fauna.client.Logging.headersAsString;
import static com.fauna.codec.Generic.pageOf;
import static com.fauna.constants.ErrorMessages.FEED_SUBSCRIPTION;
import static com.fauna.constants.ErrorMessages.QUERY_EXECUTION;
import static com.fauna.constants.ErrorMessages.QUERY_PAGE;
import static com.fauna.constants.ErrorMessages.STREAM_SUBSCRIPTION;
/**
* A client to interact with the Fauna service, providing asynchronous and synchronous query execution,
* pagination, and streaming features.
*/
public abstract class FaunaClient {
public static final RetryStrategy DEFAULT_RETRY_STRATEGY =
ExponentialBackoffStrategy.builder().build();
public static final RetryStrategy NO_RETRY_STRATEGY = new NoRetryStrategy();
private final String faunaSecret;
private final CodecProvider codecProvider =
new DefaultCodecProvider(new DefaultCodecRegistry());
private final AtomicLong lastTransactionTs = new AtomicLong(-1);
private final Logger logger;
private final StatsCollector statsCollector;
abstract RetryStrategy getRetryStrategy();
abstract HttpClient getHttpClient();
abstract RequestBuilder getRequestBuilder();
abstract RequestBuilder getStreamRequestBuilder();
abstract RequestBuilder getFeedRequestBuilder();
/**
* Constructs a FaunaClient with the provided secret and logger.
*
* @param secret The Fauna secret used for authentication.
* @param logger The logger instance.
* @param statsCollector A collector for tracking statistics.
*/
public FaunaClient(final String secret, final Logger logger,
final StatsCollector statsCollector) {
this.faunaSecret = secret;
this.logger = logger;
this.statsCollector = statsCollector;
}
/**
* Constructs a FaunaClient with the provided secret and log handler.
*
* @param secret The Fauna secret used for authentication.
* @param logHandler The handler to manage log outputs.
* @param statsCollector A collector for tracking statistics.
*/
public FaunaClient(final String secret, final Handler logHandler,
final StatsCollector statsCollector) {
this.faunaSecret = secret;
this.logger = Logger.getLogger(this.getClass().getCanonicalName());
this.logger.addHandler(logHandler);
this.logger.setLevel(logHandler.getLevel());
this.statsCollector = statsCollector;
}
/**
* Retrieves the Fauna secret used for authentication.
*
* @return The Fauna secret.
*/
protected String getFaunaSecret() {
return this.faunaSecret;
}
/**
* Retrieves the logger used for logging Fauna client activity.
*
* @return The logger instance.
*/
public Logger getLogger() {
return this.logger;
}
/**
* Retrieves the stats collector instance.
*
* @return The stats collector instance.
*/
public StatsCollector getStatsCollector() {
return this.statsCollector;
}
/**
* Retrieves the last known transaction timestamp.
*
* @return An Optional containing the last transaction timestamp, if available.
*/
public Optional getLastTransactionTs() {
long ts = lastTransactionTs.get();
return ts > 0 ? Optional.of(ts) : Optional.empty();
}
private static Optional extractServiceException(
final Throwable throwable) {
if (throwable instanceof ServiceException) {
return Optional.of((ServiceException) throwable);
} else if (throwable.getCause() instanceof ServiceException) {
return Optional.of((ServiceException) throwable.getCause());
} else {
return Optional.empty();
}
}
private void updateTs(final QueryResponse resp) {
Long newTs = resp.getLastSeenTxn();
if (newTs != null) {
this.lastTransactionTs.updateAndGet(
oldTs -> newTs > oldTs ? newTs : oldTs);
}
}
private void completeRequest(final QuerySuccess success,
final Throwable throwable) {
if (success != null) {
updateTs(success);
} else if (throwable != null) {
extractServiceException(throwable).ifPresent(
exc -> updateTs(exc.getResponse()));
}
}
private void completeFeedRequest(final FeedPage success,
final Throwable throwable) {
// Feeds do not update the clients latest transaction timestamp.
if (throwable != null) {
extractServiceException(throwable).ifPresent(
exc -> updateTs(exc.getResponse()));
}
}
private void logResponse(final HttpResponse response) {
logger.fine(MessageFormat.format(
"Fauna HTTP Response {0} from {1}, headers: {2}",
response.statusCode(), response.uri(),
headersAsString(response.headers())));
}
private Supplier>> makeAsyncRequest(
final HttpClient client, final HttpRequest request, final Codec codec) {
return () -> client.sendAsync(request,
HttpResponse.BodyHandlers.ofInputStream()).thenApply(
response -> {
logResponse(response);
return QueryResponse.parseResponse(response, codec,
statsCollector);
}).whenComplete(this::completeRequest);
}
private Supplier>> makeAsyncFeedRequest(
final HttpClient client, final HttpRequest request, final Codec codec) {
return () -> client.sendAsync(request,
HttpResponse.BodyHandlers.ofInputStream()).thenApply(
response -> {
logResponse(response);
return FeedPage.parseResponse(response, codec,
statsCollector);
}).whenComplete(this::completeFeedRequest);
}
private R completeAsync(final CompletableFuture future, final String executionMessage) {
try {
return future.get();
} catch (ExecutionException | InterruptedException exc) {
if (exc.getCause() != null && exc.getCause() instanceof FaunaException) {
throw (FaunaException) exc.getCause();
} else {
logger.warning(
"Execution|InterruptedException: " + exc.getMessage());
throw new ClientException(executionMessage, exc);
}
}
}
//region Asynchronous API
/**
* Sends an asynchronous Fauna Query Language (FQL) query to Fauna.
*
* var future = client.asyncQuery(fql);
* ... do some other stuff ...
* var result = future.get().getData();
*
* @param fql The FQL query to be executed.
* @return QuerySuccess The successful query result.
* @throws FaunaException If the query does not succeed, an exception will be thrown.
*/
public CompletableFuture> asyncQuery(final Query fql) {
if (Objects.isNull(fql)) {
throw new IllegalArgumentException(
"The provided FQL query is null.");
}
Codec